hsass 0.3.0 → 0.8.0
raw patch · 20 files changed
Files
- CHANGELOG.md +52/−0
- Text/Sass.hs +32/−17
- Text/Sass/Compilation.hs +54/−20
- Text/Sass/Functions.hs +35/−17
- Text/Sass/Functions/Internal.hs +20/−32
- Text/Sass/Internal.hs +1/−0
- Text/Sass/Marshal/Internal.hs +43/−0
- Text/Sass/Options.hs +40/−22
- Text/Sass/Options/Internal.hs +17/−11
- Text/Sass/Utils.hs +7/−5
- Text/Sass/Values.hs +1/−1
- Text/Sass/Values/Internal.hs +13/−13
- Text/Sass/Values/Utils.hs +31/−0
- hsass.cabal +12/−2
- test/Text/Sass/CompilationSpec.hs +155/−0
- test/Text/Sass/EncodingSpec.hs +136/−0
- test/Text/Sass/FunctionsSpec.hs +74/−0
- test/Text/Sass/TestingUtils.hs +8/−0
- test/Text/Sass/TutorialSpec.hs +93/−0
- test/Text/Sass/ValuesSpec.hs +95/−0
CHANGELOG.md view
@@ -2,6 +2,51 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.8.0] - 2018-11-25+### Changed+- Fix invalid usage of `sass_make_import` and fix `SassImport`. This is a+ breaking change. Fixes [#12](https://github.com/jakubfijalkowski/hsass/issues/12).++## [0.7.0] - 2018-04-02+### Changed+- This library uses UTF-8 to encode/decode all `CString`s (paths included!)++## [0.6.0] - 2018-03-24+### Changed+- Switched to `hLibsass` 0.1.7.0 and `libsass` 3.5.2++### Added+- `compileByteString` for `ByteString`-only compilation+- option to strip BOM/@charset with `sassStripEncodingInfo` option (thanks @h-3-0 !)++### Removed+- `clearNativeFunction` and `clearNativeFunctionList` are not needed anymore++## [0.5.0] - 2017-11-01+### Changed+- Split `SassImporter` into `SassImporter` and `SassHeader` (thanks @h-3-0 !)++### Added+- `SassIporter` now takes path to the file that is being imported into (thanks+ @h-3-0)++## [0.4.2] - 2017-08-28+### Added+- Introduce `defaultSassOptions` as default options (thanks @chris-martin !)++## [0.4.1] - 2017-03-19+### Fixed+- Bump `hlibsass` version to 0.1.5.2 (fixes build problem on Cabal 1.24 & Stack+ 1.4)++## [0.4.0] - 2015-12-19+### Added+- Support for combining `SassValues` (new in hlibsass 0.1.5)++### Changed+- Requires hlibsass version 0.1.5+- `SassValue` derives `Show`+ ## [0.3.0] - 2015-07-10 ### Added - Support for a `ByteString` result (thanks to [Andy@@ -33,3 +78,10 @@ [0.2.0]: https://github.com/jakubfijalkowski/hsass/compare/v0.1.0...v0.2.0 [0.3.0]: https://github.com/jakubfijalkowski/hsass/compare/v0.2.0...v0.3.0+[0.4.0]: https://github.com/jakubfijalkowski/hsass/compare/v0.3.0...v0.4.0+[0.4.1]: https://github.com/jakubfijalkowski/hsass/compare/v0.4.0...v0.4.1+[0.4.2]: https://github.com/jakubfijalkowski/hsass/compare/v0.4.1...v0.4.2+[0.5.0]: https://github.com/jakubfijalkowski/hsass/compare/v0.4.2...v0.5.0+[0.6.0]: https://github.com/jakubfijalkowski/hsass/compare/v0.5.0...v0.6.0+[0.7.0]: https://github.com/jakubfijalkowski/hsass/compare/v0.6.0...v0.7.0+[0.8.0]: https://github.com/jakubfijalkowski/hsass/compare/v0.7.0...v0.8.0
Text/Sass.hs view
@@ -70,6 +70,11 @@ -- If you want access to the extended result, you have to use 'resultString', -- 'resultIncludes' and 'resultSourcemap' on the result to extract desirable -- information.+--+-- Note that if the compiled output contains non-ASCII characters, by default it+-- will be prefixed with either a @\@charset@ rule indicating UTF-8 encoding or+-- a byte-order mark (depending on the output style). If this is inconvenient,+-- you can suppress it by enabling 'sassStripEncodingInfo'. -- $options -- 'SassOptions' wraps <http://libsass.org libsass> context options. It does not@@ -141,16 +146,15 @@ -- documentation for more information. -- $headers_and_importers--- Importers are functions that override default behaviour of @import statements--- For example, you may implement rewrite rules or even download stylesheets--- from remote server.+-- Importers are functions that override default behaviour of @@import@+-- statements. For example, you may implement rewrite rules or even download+-- stylesheets from a remote server. ----- Headers leverage the same infrastructure as importers - they are just used--- not for @imports, but for every file being loaded. They allow you to inject--- arbitrary sass source in a file.+-- Headers allow you to insert arbitrary sass at the beginning of the file being+-- compiled. ----- Let's say that we want to inject a path to currently compiled file. We may--- write the following header:+-- Let's say that we want to inject a path to the currently compiled file. We+-- may write the following header: -- -- > header src = return [makeSourceImport $ "$file: " ++ src ++ ";"] --@@ -158,10 +162,10 @@ -- the current file (first argument of the function). -- Then, we must define header signature and override options: ----- > headerSig = SassImporter 1 header+-- > headerSig = SassHeader 1 header -- > opts = def { sassHeaders = Just [headerSig], sassInputPath = Just "path" } ----- We set 'sassInputPath', because we will be compiling string and it won't be+-- We set 'sassInputPath', because we will be compiling a string and it won't be -- set automatically. -- Now, executing --@@ -171,15 +175,26 @@ -- -- > "foo { prop: path; }" ----- Importers are defined and act similarly - they just take path to the file--- being imported instead of file being processed and are injected using--- 'sassImporters'.+-- Importers are defined and act similarly, but they take two arguments. The+-- first argument is the path to the file being imported, and the second+-- argument is the path to the importing file. For example ----- Additionally, importers support priorities - if two importers return source--- for some file, the one with higher priority wins. For example+-- > importer imp src = return+-- > [makeSourceImport $ "/* imported " ++ imp ++ " into " ++ src ++ " */"]+-- > sassImporter = Just [SassImporter 1 importer]+-- > opts = def { sassImporters = sassImporter, sassInputPath = Just "file" }+-- > compileString "@import \"relative/path\"" opts ----- > importer1 src = return [makeSourceImport $ "$file: " ++ src ++ "1;"]--- > importer2 src = return [makeSourceImport $ "$file: " ++ src ++ "2;"]+-- Gives+--+-- > "/* imported relative/path into file */"+--+-- The first argument to 'SassHeader' or 'SassImporter' is its priority - if two+-- importers return source for some file, the one with higher priority wins. For+-- example+--+-- > importer1 imp _ = return [makeSourceImport $ "$file: " ++ imp ++ "1;"]+-- > importer2 imp _ = return [makeSourceImport $ "$file: " ++ imp ++ "2;"] -- > importerSigs = [SassImporter 0.5 importer1, SassImporter 1 importer2] -- > opts = def { sassImporters = Just importerSigs } -- > compileString "@import \"file\";\nfoo { prop: $file; }" opts
Text/Sass/Compilation.hs view
@@ -2,12 +2,14 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE PatternGuards #-} {-# LANGUAGE TypeSynonymInstances #-} module Text.Sass.Compilation ( -- * Compilation compileFile , compileString+ , compileByteString -- * Results , SassExtendedResult , StringResult@@ -28,12 +30,17 @@ , errorColumn ) where -import qualified Bindings.Libsass as Lib-import Data.ByteString (ByteString, packCString)+import qualified Bindings.Libsass as Lib+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as B.C8+import qualified Data.ByteString.Unsafe as B+import Data.List (stripPrefix)+import Data.Maybe (fromMaybe) #if !MIN_VERSION_base(4,8,0)-import Control.Applicative ((<$>))+import Control.Applicative ((<$>)) #endif-import Control.Monad (forM, (>=>))+import Control.Monad (forM, (>=>)) import Foreign import Foreign.C import Text.Sass.Internal@@ -78,7 +85,7 @@ -- 'SassResult'). The first provides only a compiled string, the latter one -- gives access to a list of included files and a source map (if available). class SassResult a where- toSassResult :: ForeignPtr Lib.SassContext -> IO a+ toSassResult :: Bool -> ForeignPtr Lib.SassContext -> IO a instance Show SassError where show (SassError s _) =@@ -92,22 +99,37 @@ -- | Only compiled code. instance SassResult String where- toSassResult ptr = withForeignPtr ptr $ \ctx -> do+ toSassResult stripEncoding ptr = withForeignPtr ptr $ \ctx -> do result <- Lib.sass_context_get_output_string ctx- !result' <- peekCString result- return result'+ !result' <- peekUTF8CString result+ return $ if stripEncoding then strip result' else result'+ where+ strip s+ | Just stripped <- stripPrefix "@charset \"UTF-8\";\n" s = stripped+ | Just stripped <- stripPrefix "\65279" s = stripped+ | otherwise = s -- | Only compiled code (UTF-8 encoding). instance SassResult ByteString where- toSassResult ptr = withForeignPtr ptr $ \ctx -> do+ toSassResult stripEncoding ptr = withForeignPtr ptr $ \ctx -> do result <- Lib.sass_context_get_output_string ctx- !result' <- packCString result- return result'+ !result' <- B.packCString result+ return $ if stripEncoding then strip result' else result'+ where+ strip s+ | Just stripped <- stripCharset s = stripped+ | Just stripped <- stripBom s = stripped+ | otherwise = s+ stripCharset = stripPrefixBS (B.C8.pack "@charset \"UTF-8\";\n")+ stripBom = stripPrefixBS (B.C8.pack "\239\187\191")+ stripPrefixBS bs1 bs2+ | bs1 `B.C8.isPrefixOf` bs2 = Just (B.unsafeDrop (B.length bs1) bs2)+ | otherwise = Nothing -- | Compiled code with includes and a source map. instance (SassResult a) => SassResult (SassExtendedResult a) where- toSassResult ptr = do- str <- toSassResult ptr+ toSassResult stripEncoding ptr = do+ str <- toSassResult stripEncoding ptr return $ SassExtendedResult str ptr -- | Loads specified property from a context and converts it to desired type.@@ -118,12 +140,12 @@ loadFromError get conv err = withForeignPtr ptr $ get >=> conv where ptr = errorContext err --- | Equivalent of @'loadFromError' 'get' 'peekCString' 'err'@.+-- | Equivalent of @'loadFromError' 'get' 'peekUTF8CString 'err'@. loadStringFromError :: (Ptr Lib.SassContext -> IO CString) -- ^ Accessor function. -> SassError -- ^ Pointer to context. -> IO String -- ^ Result.-loadStringFromError get = loadFromError get peekCString+loadStringFromError get = loadFromError get peekUTF8CString -- | Equivalent of @'loadFromError' 'get' 'fromInteger' 'err'@. loadIntFromError :: (Integral a)@@ -165,7 +187,7 @@ resultIncludes ex = withForeignPtr (resultContext ex) $ \ctx -> do lst <- Lib.sass_context_get_included_files ctx len <- Lib.sass_context_get_included_files_size ctx- forM (arrayRange $ fromIntegral len) (peekElemOff lst >=> peekCString)+ forM (arrayRange $ fromIntegral len) (peekElemOff lst >=> peekUTF8CString) -- | Loads a source map if it was generated by libsass. resultSourcemap :: SassExtendedResult a -> IO (Maybe String)@@ -173,7 +195,7 @@ cstr <- Lib.sass_context_get_source_map_string ctx if cstr == nullPtr then return Nothing- else Just <$> peekCString cstr+ else Just <$> peekUTF8CString cstr -- | Common code for 'compileFile' and 'compileString'. compileInternal :: (SassResult b)@@ -195,7 +217,7 @@ then return $ Left $ SassError (fromIntegral status) fptr else do- result <- toSassResult fptr+ result <- toSassResult (sassStripEncodingInfo opts) fptr return $ Right result -- | Compiles a file using specified options.@@ -203,7 +225,7 @@ => FilePath -- ^ Path to the file. -> SassOptions -- ^ Compilation options. -> IO (Either SassError a) -- ^ Error or output string.-compileFile path opts = withCString path $ \cpath ->+compileFile path opts = withUTF8CString path $ \cpath -> compileInternal cpath opts Lib.sass_make_file_context Lib.sass_compile_file_context@@ -215,7 +237,19 @@ -> SassOptions -- ^ Compilation options. -> IO (Either SassError a) -- ^ Error or output string. compileString str opts = do- cdata <- newCString str+ cdata <- newUTF8CString str+ compileInternal cdata opts+ Lib.sass_make_data_context+ Lib.sass_compile_data_context+ Lib.p_sass_delete_data_context++-- | Compiles raw Sass content using specified options.+compileByteString :: SassResult a+ => ByteString -- ^ String to compile.+ -> SassOptions -- ^ Compilation options.+ -> IO (Either SassError a) -- ^ Error or output string.+compileByteString str opts = do+ cdata <- newCStringFromBS str compileInternal cdata opts Lib.sass_make_data_context Lib.sass_compile_data_context
Text/Sass/Functions.hs view
@@ -8,6 +8,8 @@ , SassImport (..) , SassImporterType , SassImporter (..)+ , SassHeaderType+ , SassHeader (..) , makeSourceImport , makePathImport ) where@@ -30,25 +32,30 @@ -- | Represents a sass import - a sass content with additional metadata. -- -- Even though this ADT has four fields, you may just provide either--- 'importPath' and 'importBase' and leave loading to the library, or provide--- 'importSource' and do not provide 'importPath' nor 'importBase'.+-- 'importPath' and 'importAbsolutePath' and leave loading to the library, or+-- provide 'importSource' and do not provide 'importAbsolutePath'. -- Nevertheless, you are free to provide all of the fields. data SassImport = SassImport {- importPath :: Maybe FilePath, -- ^ Path to the import, relative to base.- importBase :: Maybe FilePath, -- ^ Base path.- importSource :: Maybe String, -- ^ Import's source.- importSourceMap :: Maybe String -- ^ Source map of the import.+ -- | Path to the import, as requested by the import statement.+ importPath :: Maybe FilePath + -- | Absolute path to the file.+ , importAbsolutePath :: Maybe FilePath + -- | Import's source.+ , importSource :: Maybe String+ -- | Source map of the import.+ , importSourceMap :: Maybe String } --- | Type of the function that acts like importer/header.+-- | Type of the function that acts like an importer. ----- You may return empty list in order to tell libsass to handle the import by--- itself or not insert any header.+-- You may return the empty list in order to tell libsass to handle the import by+-- itself. type SassImporterType =- String- -- ^ Path to the import that needs to be loaded or file that is being- -- processed when used as a header.- -> IO [SassImport] -- ^ Imports.+ String+ -- ^ Path to the import that needs to be loaded.+ -> String+ -- ^ Absolute path to the importing file.+ -> IO [SassImport] -- ^ Imports. -- | Description of the importer. data SassImporter = SassImporter {@@ -56,12 +63,23 @@ , importerFunction :: SassImporterType -- ^ Main function. } +-- | Type of the function that acts like a header.+type SassHeaderType =+ String -- ^ Absolute path to the file being processed.+ -> IO [SassImport] -- ^ Imports.++-- | Description of the header.+data SassHeader = SassHeader {+ headerPriority :: Double -- ^ Priority of the header.+ , headerFunction :: SassHeaderType -- ^ Main function.+}+ -- | 'makeSourceImport' @s@ is equivalent to 'SassImport' -- @Nothing Nothing (Just s) Nothing@. makeSourceImport :: String -> SassImport makeSourceImport s = SassImport Nothing Nothing (Just s) Nothing --- | 'makePathImport' @p b@ is equivalent to 'SassImport'--- @(Just p) (Just b) Nothing Nothing@.-makePathImport :: String -> String -> SassImport-makePathImport p b = SassImport (Just p) (Just b) Nothing Nothing+-- | 'makePathImport' @p@ is equivalent to 'SassImport'+-- @(Just p) (Just p) Nothing Nothing@.+makePathImport :: String -> SassImport+makePathImport p = SassImport (Just p) (Just p) Nothing Nothing
Text/Sass/Functions/Internal.hs view
@@ -1,13 +1,10 @@-{-# LANGUAGE LambdaCase #-} module Text.Sass.Functions.Internal ( -- * Functions wrapFunction , makeNativeFunction- , clearNativeFunction , freeNativeFunction , makeNativeFunctionList- , clearNativeFunctionList , freeNativeFunctionList -- * Imports and headers , wrapImporter@@ -21,10 +18,10 @@ , freeNativeImporterList ) where -import qualified Bindings.Libsass as Lib+import qualified Bindings.Libsass as Lib import Foreign-import Foreign.C import Text.Sass.Functions+import Text.Sass.Marshal.Internal import Text.Sass.Utils import Text.Sass.Values.Internal @@ -35,27 +32,17 @@ -- | Converts 'SassFunction' into native representation. ----- Freeing native representation is not a pleasant process - libsass frees--- the 'Lib.SassFunctionEntry', but does not free signature. Because of that,--- special care must be taken in order to properly deallocate the object. -- If you don't want to pass the resulting object to Sass_Options,--- call both 'clearNativeFunction' and then 'freeNativeFunction'. Otherwise,--- you should call 'clearNativeFunction' BEFORE you deallocate context.+-- call 'freeNativeFunction'. makeNativeFunction :: SassFunction -> IO Lib.SassFunctionEntry makeNativeFunction (SassFunction sig' fn) = do- sig <- newCString sig'+ sig <- newUTF8CString sig' wrapped <- Lib.mkSassFunctionFn $ wrapFunction fn Lib.sass_make_function sig wrapped nullPtr --- | Releases the signature of a function entry.-clearNativeFunction :: Lib.SassFunctionEntry -> IO ()-clearNativeFunction entry = do- sig <- Lib.sass_function_get_signature entry- free sig- -- | Deallocates the object, but does not deallocate signature. freeNativeFunction :: Lib.SassFunctionEntry -> IO ()-freeNativeFunction = free+freeNativeFunction = Lib.sass_delete_function -- | Converts list of 'SassFunction's into native representation. --@@ -65,28 +52,29 @@ makeNativeFunctionList = copyToCList Lib.sass_make_function_list makeNativeFunction pokeElemOff --- | Releases signatures of entries in the list.-clearNativeFunctionList :: Lib.SassFunctionList -> IO ()-clearNativeFunctionList = loopCList clearNativeFunction- -- | Frees the list and entries, without releasing signatures. freeNativeFunctionList :: Lib.SassFunctionList -> IO ()-freeNativeFunctionList = loopCList freeNativeFunction+freeNativeFunctionList = Lib.sass_delete_function_list -- | Wraps function of type 'SassImporterType'. wrapImporter :: SassImporterType -> Lib.SassImporterFnType-wrapImporter fn url _ _ = peekCString url >>= fn >>= \case- [] -> return nullPtr- xs -> makeNativeImportList xs+wrapImporter fn url _ compiler = do+ lastImport <- Lib.sass_compiler_get_last_import compiler+ absPath <- Lib.sass_import_get_abs_path lastImport >>= peekUTF8CString+ url' <- peekUTF8CString url+ importList <- fn url' absPath+ case importList of+ [] -> return nullPtr+ xs -> makeNativeImportList xs -- | Converts 'SassImport' into native representation. makeNativeImport :: SassImport -> IO Lib.SassImportEntry-makeNativeImport el = do- path <- maybeNew newCString $ importPath el- base <- maybeNew newCString $ importPath el- source <- maybeNew newCString $ importSource el- srcmap <- maybeNew newCString $ importSourceMap el- Lib.sass_make_import path base source srcmap+makeNativeImport el =+ maybeWith withUTF8CString (importPath el) $ \path ->+ maybeWith withUTF8CString (importAbsolutePath el) $ \absPath -> do+ source <- maybeNew newUTF8CString $ importSource el+ srcmap <- maybeNew newUTF8CString $ importSourceMap el+ Lib.sass_make_import path absPath source srcmap -- | Frees native representation of 'SassImport'. freeNativeImport :: Lib.SassImportEntry -> IO ()
Text/Sass/Internal.hs view
@@ -5,6 +5,7 @@ ) where import Text.Sass.Functions.Internal as I+import Text.Sass.Marshal.Internal as I import Text.Sass.Options.Internal as I import Text.Sass.Utils as I import Text.Sass.Values.Internal as I
+ Text/Sass/Marshal/Internal.hs view
@@ -0,0 +1,43 @@+-- | Helper functions used by Compilation module.+module Text.Sass.Marshal.Internal+ (+ peekUTF8CString,+ newUTF8CString,+ withUTF8CString,+ newCStringFromBS+ ) where++import Data.ByteString (ByteString)+import qualified Data.ByteString.Internal as BI+import Foreign+import Foreign.C+import Foreign.Marshal.Alloc (mallocBytes)++import qualified GHC.Foreign as GHC+import qualified GHC.IO.Encoding as E++-- | Marshal a NUL terminated C string (UTF-8 encoded) into Haskell string.+-- It is equivalent to 'peekCString' but with different encoding.+peekUTF8CString :: CString -> IO String+peekUTF8CString = GHC.peekCString E.utf8++-- | Marshal a Haskell string into a NUL terminated C string (UTF-8 encoded).+-- It is equivalent to 'newCString' but with different encoding.+newUTF8CString :: String -> IO CString+newUTF8CString = GHC.newCString E.utf8++-- | Marshal a Haskell string into a C string (i.e., character array)+-- in temporary storage, with explicit length information.+-- It is equivalent to 'withCString' but with different encoding.+withUTF8CString :: String -> (CString -> IO a) -> IO a+withUTF8CString = GHC.withCString E.utf8++-- | Copies 'ByteString' to newly allocated 'CString'. The result must be+-- explicitly freed using 'free' or 'finalizerFree'.+newCStringFromBS :: ByteString -> IO CString+newCStringFromBS (BI.PS fp o l) = do+ buf <- mallocBytes (l + 1)+ withForeignPtr fp $ \p -> do+ BI.memcpy buf (p `plusPtr` o) (fromIntegral l)+ pokeByteOff buf l (0::Word8)+ return $ castPtr buf
Text/Sass/Options.hs view
@@ -2,6 +2,7 @@ module Text.Sass.Options ( SassOptions (..)+ , defaultSassOptions , Lib.SassOutputStyle (..) ) where @@ -9,7 +10,9 @@ import Data.Default.Class import Text.Sass.Functions --- | Describes options used by libsass during compilation.+-- | Describes compilation options. With the exception of+-- 'sassStripEncodingInfo', these correspond to the compilation options of+-- libsass. data SassOptions = SassOptions { -- | Precision of fractional numbers. sassPrecision :: Int@@ -50,29 +53,44 @@ -- | List of user-supplied functions that provide "headers" for sass files. -- Header is injected at the beginning of a file which name is passed as -- the first argument of importer.- , sassHeaders :: Maybe [SassImporter]+ , sassHeaders :: Maybe [SassHeader] -- | List of user-supplied functions that resolve @import directives. , sassImporters :: Maybe [SassImporter]+ -- | Remove @\@charset \"UTF-8\";\\n@ or byte-order mark from CSS output,+ -- if present.+ , sassStripEncodingInfo :: Bool } +-- | The default 'SassOptions':+--+-- * 'sassPrecision' = 5+-- * 'sassOutputStyle' = 'Lib.SassStyleNested'+-- * 'sassIndent' = two spaces+-- * 'sassLinefeed' = @"\\n"@+-- * All other fields default to 'False' or 'Nothing'.+defaultSassOptions :: SassOptions+defaultSassOptions = SassOptions+ { sassPrecision = 5+ , sassOutputStyle = Lib.SassStyleNested+ , sassSourceComments = False+ , sassSourceMapEmbed = False+ , sassSourceMapContents = False+ , sassOmitSourceMapUrl = False+ , sassIsIndentedSyntax = False+ , sassIndent = " "+ , sassLinefeed = "\n"+ , sassInputPath = Nothing+ , sassOutputPath = Nothing+ , sassPluginPaths = Nothing+ , sassIncludePaths = Nothing+ , sassSourceMapFile = Nothing+ , sassSourceMapRoot = Nothing+ , sassFunctions = Nothing+ , sassHeaders = Nothing+ , sassImporters = Nothing+ , sassStripEncodingInfo = False+ }++-- | 'def' = 'defaultSassOptions' instance Default SassOptions where- def = SassOptions {- sassPrecision = 5- , sassOutputStyle = Lib.SassStyleNested- , sassSourceComments = False- , sassSourceMapEmbed = False- , sassSourceMapContents = False- , sassOmitSourceMapUrl = False- , sassIsIndentedSyntax = False- , sassIndent = " "- , sassLinefeed = "\n"- , sassInputPath = Nothing- , sassOutputPath = Nothing- , sassPluginPaths = Nothing- , sassIncludePaths = Nothing- , sassSourceMapFile = Nothing- , sassSourceMapRoot = Nothing- , sassFunctions = Nothing- , sassHeaders = Nothing- , sassImporters = Nothing- }+ def = defaultSassOptions
Text/Sass/Options/Internal.hs view
@@ -14,8 +14,9 @@ #endif import Control.Monad ((>=>)) import Foreign-import Foreign.C+import Text.Sass.Functions import Text.Sass.Functions.Internal+import Text.Sass.Marshal.Internal import Text.Sass.Options import Text.Sass.Utils @@ -30,27 +31,31 @@ Lib.sass_option_set_source_map_contents ptr (sassSourceMapContents opt) Lib.sass_option_set_omit_source_map_url ptr (sassOmitSourceMapUrl opt) Lib.sass_option_set_is_indented_syntax_src ptr (sassIsIndentedSyntax opt)- withCString (sassIndent opt) (Lib.sass_option_set_indent ptr)- withCString (sassLinefeed opt) (Lib.sass_option_set_linefeed ptr)- withOptionalCString (sassInputPath opt)+ withUTF8CString (sassIndent opt) (Lib.sass_option_set_indent ptr)+ withUTF8CString (sassLinefeed opt) (Lib.sass_option_set_linefeed ptr)+ withOptionalUTF8CString (sassInputPath opt) (Lib.sass_option_set_input_path ptr)- withOptionalCString (sassOutputPath opt)+ withOptionalUTF8CString (sassOutputPath opt) (Lib.sass_option_set_output_path ptr)- withOptionalCString (concatPaths <$> sassPluginPaths opt)+ withOptionalUTF8CString (concatPaths <$> sassPluginPaths opt) (Lib.sass_option_set_plugin_path ptr)- withOptionalCString (concatPaths <$> sassIncludePaths opt)+ withOptionalUTF8CString (concatPaths <$> sassIncludePaths opt) (Lib.sass_option_set_include_path ptr)- withOptionalCString (sassSourceMapFile opt)+ withOptionalUTF8CString (sassSourceMapFile opt) (Lib.sass_option_set_source_map_file ptr)- withOptionalCString (sassSourceMapRoot opt)+ withOptionalUTF8CString (sassSourceMapRoot opt) (Lib.sass_option_set_source_map_root ptr) maybe (return ())- (makeNativeImporterList >=> Lib.sass_option_set_c_headers ptr)+ (makeNativeImporterList . fmap fromHeader+ >=> Lib.sass_option_set_c_headers ptr) (sassHeaders opt) maybe (return ()) (makeNativeImporterList >=> Lib.sass_option_set_c_importers ptr) (sassImporters opt)+ where+ fromHeader (SassHeader p f) = SassImporter p (\filename _ -> f filename) + -- | Copies 'sassFunctions' to native object, executes action, clears leftovers -- (see documentation of 'makeNativeFunction') and returns action result. withFunctions :: SassOptions -- ^ Options.@@ -64,5 +69,6 @@ nativeFnList <- makeNativeFunctionList lst Lib.sass_option_set_c_functions ptr nativeFnList !result <- action- clearNativeFunctionList nativeFnList+ Lib.sass_option_set_c_functions ptr nullPtr+ freeNativeFunctionList nativeFnList return result
Text/Sass/Utils.hs view
@@ -4,7 +4,7 @@ module Text.Sass.Utils ( -- * Interoperation with C API- withOptionalCString+ withOptionalUTF8CString , listEntryNotNull , loopCList , copyToCList@@ -23,6 +23,8 @@ import Foreign.C import System.FilePath (searchPathSeparator) +import Text.Sass.Marshal.Internal+ -- Fix for transformers-0.3.0.0 (used by lts-2.17 in stack). #if !MIN_VERSION_transformers(0,4,0) modify' :: (Monad m) => (s -> s) -> StateT s m ()@@ -31,11 +33,11 @@ put $! f s #endif --- | 'withOptionalCString' @str action@, if @str@ is 'Nothing', @action@ is not+-- | 'withOptionalCString' @str action@, if @str@ is @Nothing@, @action@ is not -- invoked, otherwise behaves like 'withCString'.-withOptionalCString :: Maybe String -> (CString -> IO ()) -> IO ()-withOptionalCString (Just str) action = withCString str action-withOptionalCString Nothing _ = return ()+withOptionalUTF8CString :: Maybe String -> (CString -> IO ()) -> IO ()+withOptionalUTF8CString (Just str) action = withUTF8CString str action+withOptionalUTF8CString Nothing _ = return () -- | Checks if the pointer in state points to non-null location. listEntryNotNull :: (Monad m, MonadIO m) => StateT (Ptr (Ptr a)) m Bool
Text/Sass/Values.hs view
@@ -28,4 +28,4 @@ | SassNull -- ^ Null value. | SassWarning String -- ^ Warning with message. | SassError String -- ^ Error with message.- deriving (Eq)+ deriving (Eq, Show)
Text/Sass/Values/Internal.hs view
@@ -9,32 +9,32 @@ , makeValueForeignPtr ) where -import qualified Bindings.Libsass as Lib+import qualified Bindings.Libsass as Lib #if !MIN_VERSION_base(4,8,0)-import Control.Applicative ((<$>))+import Control.Applicative ((<$>)) #endif-import Control.Monad (forM, (>=>))+import Control.Monad (forM, (>=>)) import Foreign-import Foreign.C+import Text.Sass.Marshal.Internal import Text.Sass.Utils import Text.Sass.Values -- | Converts a 'SassValue' to native type. toNativeValue :: SassValue -> IO (Ptr Lib.SassValue) toNativeValue (SassBool val) = Lib.sass_make_boolean val-toNativeValue (SassNumber val unit) = withCString unit $+toNativeValue (SassNumber val unit) = withUTF8CString unit $ Lib.sass_make_number (realToFrac val) toNativeValue (SassColor r g b a) = Lib.sass_make_color r' g' b' a' where r' = realToFrac r g' = realToFrac g b' = realToFrac b a' = realToFrac a-toNativeValue (SassString str) = withCString str Lib.sass_make_string+toNativeValue (SassString str) = withUTF8CString str Lib.sass_make_string toNativeValue SassNull = Lib.sass_make_null-toNativeValue (SassWarning str) = withCString str Lib.sass_make_warning-toNativeValue (SassError str) = withCString str Lib.sass_make_error+toNativeValue (SassWarning str) = withUTF8CString str Lib.sass_make_warning+toNativeValue (SassError str) = withUTF8CString str Lib.sass_make_error toNativeValue (SassList lst sep') =- copyToCList (`Lib.sass_make_list` sep) toNativeValue+ copyToCList (\size -> Lib.sass_make_list size sep False) toNativeValue Lib.sass_list_set_value lst where sep = fromIntegral $ fromEnum sep' @@ -60,7 +60,7 @@ SassBool <$> Lib.sass_boolean_get_value ptr fromNativeValue' Lib.SassNumber ptr = do val <- Lib.sass_number_get_value ptr- unit <- Lib.sass_number_get_unit ptr >>= peekCString+ unit <- Lib.sass_number_get_unit ptr >>= peekUTF8CString return $ SassNumber (realToFrac val) unit fromNativeValue' Lib.SassColor ptr = do r <- realToFrac <$> Lib.sass_color_get_r ptr@@ -69,12 +69,12 @@ a <- realToFrac <$> Lib.sass_color_get_a ptr return $ SassColor r g b a fromNativeValue' Lib.SassString ptr =- SassString <$> (Lib.sass_string_get_value ptr >>= peekCString)+ SassString <$> (Lib.sass_string_get_value ptr >>= peekUTF8CString) fromNativeValue' Lib.SassNull _ = return SassNull fromNativeValue' Lib.SassWarning ptr =- SassWarning <$> (Lib.sass_warning_get_message ptr >>= peekCString)+ SassWarning <$> (Lib.sass_warning_get_message ptr >>= peekUTF8CString) fromNativeValue' Lib.SassError ptr =- SassError <$> (Lib.sass_error_get_message ptr >>= peekCString)+ SassError <$> (Lib.sass_error_get_message ptr >>= peekUTF8CString) fromNativeValue' Lib.SassList ptr = do len <- Lib.sass_list_get_length ptr sep <- fromIntegral <$> Lib.sass_list_get_separator ptr
+ Text/Sass/Values/Utils.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE BangPatterns #-}+-- | Provides utility functions for working with 'SassValues'+module Text.Sass.Values.Utils+ (+ combineSassValues+ , Lib.SassOp (..)+ ) where++import qualified Bindings.Libsass as Lib++import System.IO.Unsafe++import Text.Sass.Values+import Text.Sass.Values.Internal++-- | Combines two 'SassValue's using specified operator.+-- |+-- | Uses 'Lib.sass_value_op'.+combineSassValues :: Lib.SassOp -- ^ Operator.+ -> SassValue -- ^ First value.+ -> SassValue -- ^ Second value.+ -> SassValue -- ^ Resulting value.+combineSassValues op val1 val2 = unsafePerformIO $ do+ native1 <- toNativeValue val1+ native2 <- toNativeValue val2+ combined <- Lib.sass_value_op (fromIntegral $ fromEnum op) native1 native2+ !result <- fromNativeValue combined+ deleteNativeValue native1+ deleteNativeValue native2+ deleteNativeValue combined+ return result
hsass.cabal view
@@ -1,5 +1,5 @@ name: hsass-version: 0.3.0+version: 0.8.0 license: MIT license-file: LICENSE author: Jakub Fijałkowski <fiolek94@gmail.com>@@ -42,12 +42,14 @@ , Text.Sass.Values , Text.Sass.Internal , Text.Sass.Functions.Internal+ , Text.Sass.Marshal.Internal , Text.Sass.Options.Internal , Text.Sass.Values.Internal+ , Text.Sass.Values.Utils , Text.Sass.Utils build-depends: base >= 4.7 && < 5- , hlibsass >= 0.1.1+ , hlibsass >= 0.1.7.0 , bytestring >= 0.10.0 , data-default-class , filepath >= 1.0@@ -61,6 +63,13 @@ test-suite test hs-source-dirs: test main-is: Spec.hs+ other-modules:+ Text.Sass.CompilationSpec+ , Text.Sass.FunctionsSpec+ , Text.Sass.TestingUtils+ , Text.Sass.TutorialSpec+ , Text.Sass.ValuesSpec+ , Text.Sass.EncodingSpec type: exitcode-stdio-1.0 ghc-options: -Wall build-depends:@@ -69,6 +78,7 @@ , hspec >= 2.1.5 , hspec-discover >= 2.1.5 , temporary >= 1.1+ , text >= 1.2 , hsass , data-default-class default-language: Haskell2010
+ test/Text/Sass/CompilationSpec.hs view
@@ -0,0 +1,155 @@+module Text.Sass.CompilationSpec where++import System.IO+import System.IO.Temp+import Test.Hspec+import Text.Sass++import Data.ByteString.Char8 (pack)+import Data.Either (isLeft, isRight)+import Data.Maybe (isJust)+import Text.Sass.TestingUtils++main :: IO ()+main = hspec spec++importerFunc :: String -> String -> IO [SassImport]+importerFunc _ _ = return [makeSourceImport "a { margin: 1px; }"]++importers :: [SassImporter]+importers = [SassImporter 1 importerFunc]++extendedResultSpec, compilationSpec, errorReportingSpec, stripEncodingInfoSpec, spec :: Spec+spec = do+ describe "Compilation" compilationSpec+ describe "Extended compilation" extendedResultSpec+ describe "Error reporting" errorReportingSpec+ describe "Stripping encoding information" stripEncodingInfoSpec++compilationSpec = do+ it "should compile simple source" $+ compileString "foo { margin: 21px * 2; }" def `shouldReturn`+ Right "foo {\n margin: 42px; }\n"++ it "should compile simple source as a bytestring" $+ compileString "foo { margin: 21px * 2; }" def `shouldReturn`+ Right (pack "foo {\n margin: 42px; }\n")++ it "should respect options" $ do+ let opts = def { sassOutputStyle = SassStyleCompressed }+ compileString "foo { margin: 21px * 2; }" opts `shouldReturn`+ Right "foo{margin:42px}\n"++ it "should compile file" $+ withSystemTempFile "styles.sass" $ \p h -> do+ hPutStr h "foo { margin: 21px * 2; }"+ hClose h+ compileFile p def `shouldReturn` Right "foo {\n margin: 42px; }\n"++ it "should respect options when compiling file" $ do+ let opts = def { sassOutputStyle = SassStyleCompressed }+ withSystemTempFile "styles.sass" $ \p h -> do+ hPutStr h "foo { margin: 21px * 2; }"+ hClose h+ compileFile p opts `shouldReturn` Right "foo{margin:42px}\n"++ it "should compile bytestring correctly" $+ compileByteString (pack "foo { margin: 21px * 2; }") def `shouldReturn`+ Right (pack "foo {\n margin: 42px; }\n")++extendedResultSpec = do+ it "should compile simple source" $ do+ res <- compileString "foo { margin: 21px * 2; }" def+ res `shouldSatisfy` isRight+ let Right res' = res+ resultString res' `shouldBe` "foo {\n margin: 42px; }\n"++ it "should compile simple source as a bytestring" $ do+ res <- compileString "foo { margin: 21px * 2; }" def+ res `shouldSatisfy` isRight+ let Right res' = res+ resultString res' `shouldBe` (pack "foo {\n margin: 42px; }\n")++ it "should report correct includes when available" $ do+ let opts = def { sassImporters = Just importers }+ Right res <- compileString "@import '_abc';" opts :: ExtendedResult+ resultIncludes res `shouldReturn` [ "_abc" ]++ it "should report no includes when unavailable" $ do+ Right res <- compileString "foo { margin: 1px; }" def :: ExtendedResult+ resultIncludes res `shouldReturn` [ ]++ it "should retrieve source map if available" $ do+ let opts = def { sassSourceMapFile = Just "abc.css" }+ Right res <- compileString "foo { margin: 1px; }" opts :: ExtendedResult+ m <- resultSourcemap res+ m `shouldSatisfy` isJust+ let Just m' = m+ m' `shouldSatisfy` (not . null)++ it "should return Nothing is source map if not available" $ do+ Right res <- compileString "foo { margin: 1px; }" def :: ExtendedResult+ resultSourcemap res `shouldReturn` Nothing++errorReportingSpec = do+ it "string compilation should report error on invalid code" $+ (compileString "inv mark" def :: StringResult)+ `returnShouldSatisfy` isLeft++ it "file compilation should report error on invalid code" $+ withSystemTempFile "styles.sass" $ \p h -> do+ hPutStr h "!@# !@## ## #"+ hClose h+ (compileFile p def :: StringResult) `returnShouldSatisfy` isLeft++ it "should contain line" $ do+ (Left r) <- compileString "invalid mark" def :: StringResult+ errorLine r `shouldReturn` 1++ it "should contain column" $ do+ (Left r) <- compileString "body { !! }" def :: StringResult+ errorColumn r `shouldReturn` 7++ it "should contain description" $ do+ (Left r) <- compileString "body { !! }" def :: StringResult+ errorText r `returnShouldSatisfy` (not . null)++ it "should contain message" $ do+ (Left r) <- compileString "body { !! }" def :: StringResult+ errorMessage r `returnShouldSatisfy` (not . null)++ it "should contain Json description" $ do+ (Left r) <- compileString "body { !! }" def :: StringResult+ errorJson r `returnShouldSatisfy` (not . null)++ it "should contain file path" $ do+ (Left r) <- compileString "body { !! }" def :: StringResult+ errorFile r `returnShouldSatisfy` (not . null)++stripEncodingInfoSpec = do+ describe "from strings" $ do+ it "should strip '@charset \"UTF-8\";\\n'" $ do+ let opts = def { sassStripEncodingInfo = True }+ compileString "h1:before { content: '\9660'; }" opts `shouldReturn`+ Right "h1:before {\n content: '\9660'; }\n"++ it "should strip byte-order mark" $ do+ let opts = def { sassOutputStyle = SassStyleCompressed+ , sassStripEncodingInfo = True+ }+ compileString "h1:before { content: '\9660'; }" opts `shouldReturn`+ Right "h1:before{content:'\9660'}\n"++ describe "from bytestrings" $ do+ it "should strip '@charset \"UTF-8\";\\n'" $ do+ let opts = def { sassStripEncodingInfo = True }+ compileString "h1:before { content: '\9660'; }" opts `shouldReturn`+ Right (pack "h1:before {\n content: '\226\150\188'; }\n")++ it "should strip byte-order mark" $ do+ let opts = def { sassOutputStyle = SassStyleCompressed+ , sassStripEncodingInfo = True+ }+ compileString "h1:before { content: '\9660'; }" opts `shouldReturn`+ Right (pack "h1:before{content:'\226\150\188'}\n")+
+ test/Text/Sass/EncodingSpec.hs view
@@ -0,0 +1,136 @@+module Text.Sass.EncodingSpec where++import Control.Concurrent.MVar+import qualified Data.ByteString as BS+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import GHC.IO.Encoding (setFileSystemEncoding, utf8)+import System.IO+import System.IO.Temp+import Test.Hspec+import Text.Sass+import qualified Text.Sass.Values.Internal as VI++opts :: SassOptions+opts = def { sassStripEncodingInfo = True }++testString :: String+testString = "Zażółcić gęślą jaźń"++testInput :: String+testInput = "a { content: '" ++ testString ++ "'; }"++expectedResult :: Either a String+expectedResult = Right $ "a {\n content: '" ++ testString ++ "'; }\n"++main :: IO ()+main = hspec spec++testValue :: SassValue -> Expectation+testValue v =+ (VI.toNativeValue v >>= VI.fromNativeValue) `shouldReturn` v++storeFunc :: MVar String -> SassValue -> IO SassValue+storeFunc m (SassList [SassString s] _) = do+ putMVar m s+ return $ SassString s+storeFunc _ _ = fail "Unknown value"++storeFuncSig :: MVar String -> SassFunction+storeFuncSig = SassFunction "store($s)" . storeFunc++returnFunc :: SassValue -> IO SassValue+returnFunc _ = return $ SassString $ "'" ++ testString ++ "'"++returnFuncSig :: SassFunction+returnFuncSig = SassFunction "ret()" returnFunc++nonUTFNameSig :: SassFunction+nonUTFNameSig = SassFunction "zażółcić()" returnFunc++importFunction :: FilePath -> String -> String -> IO [SassImport]+importFunction _ "src" _ = return [makeSourceImport testInput]+importFunction p "path" _ = return [makePathImport p]+importFunction _ _ _ = fail "Unknown import"++importers :: FilePath -> [SassImporter]+importers p = [SassImporter 1 $ importFunction p]++withNonASCIIContentFile :: (String -> IO a) -> IO a+withNonASCIIContentFile f =+ withSystemTempFile "styles.scss" $ \p h -> do+ BS.hPutStr h (TE.encodeUtf8 $ T.pack testInput)+ hClose h+ f p++withNonASCIIPathFile :: (String -> IO a) -> IO a+withNonASCIIPathFile f = do+ setFileSystemEncoding utf8+ res <- withSystemTempFile "pl-zażółcić.scss" $ \p h -> do+ BS.hPutStr h (TE.encodeUtf8 $ T.pack testInput)+ hClose h+ f p+ return res++spec :: Spec+spec = do+ it "should preserve UTF-8 encoding when compiling Strings" $+ compileString testInput opts `shouldReturn` expectedResult++ it "should preserve UTF-8 encoding when compiling files" $+ withNonASCIIContentFile $ \p ->+ compileFile p opts `shouldReturn` expectedResult++ it "should correctly encode UTF-8 chars in source paths" $+ withNonASCIIPathFile $ \p ->+ compileFile p opts `shouldReturn` expectedResult++ it "should correctly encode UTF-8 chars in included files path" $+ withNonASCIIPathFile $ \p ->+ withSystemTempFile "styles.scss" $ \impP impH -> do+ let cnt = "@import \"" ++ p ++ "\";"+ BS.hPutStr impH $ TE.encodeUtf8 $ T.pack cnt+ hClose impH++ Right res <- compileFile impP opts :: ExtendedResult+ includes <- resultIncludes res+ includes `shouldSatisfy` elem p++ it "should correctly encode/decode UTF-8 SassStrings" $+ testValue $ SassString testString++ it "should correctly encode/decode UTF-8 units in SassNumbers" $+ testValue $ SassNumber 1 testString++ it "should correctly encode/decode UTF-8 SassWarnings" $+ testValue $ SassWarning testString++ it "should correctly encode/decode UTF-8 SassErrors" $+ testValue $ SassError testString++ it "should correctly encode/decode list of UTF-8 SassStrings" $+ testValue $ SassList [SassString "Zażółcić", SassString "gęślą"] SassSeparatorComma++ it "should correctly UTF-8 decode function arguments" $ do+ arg <- newEmptyMVar+ let fopts = opts { sassFunctions = Just [ storeFuncSig arg ] }+ _ <- compileString ("a {foo: store('" ++ testString ++ "');}") fopts :: StringResult+ tryTakeMVar arg `shouldReturn` Just testString++ it "should correctly UTF-8 encode function return value" $ do+ let fopts = opts { sassFunctions = Just [ returnFuncSig ] }+ compileString "a {content: ret(); }" fopts `shouldReturn` expectedResult++ it "should correctly UTF-8 encode function names" $ do+ let fopts = opts { sassFunctions = Just [ nonUTFNameSig ] }+ compileString "a {content: zażółcić(); }" fopts+ `shouldReturn` expectedResult++ it "should correctly encode UTF-8 string from importer" $+ let iopts = opts { sassImporters = Just $ importers "" }+ in compileString "@import 'src';" iopts `shouldReturn` expectedResult++ it "should correctly encode UTF-8 path from importer" $+ withNonASCIIPathFile $ \p ->+ let iopts = opts { sassImporters = Just $ importers p }+ in compileString "@import 'path';" iopts `shouldReturn` expectedResult
+ test/Text/Sass/FunctionsSpec.hs view
@@ -0,0 +1,74 @@+module Text.Sass.FunctionsSpec where++import Test.Hspec+import Text.Sass hiding (headerFunction)++fooFunction :: SassValue -> IO SassValue+fooFunction _ = return $ SassNumber 1 "px"++barFunction :: SassValue -> IO SassValue+barFunction (SassList [SassString "a"] _) = return $ SassNumber 1 "px"+barFunction _ = return $ SassError "invalid arguments"++functions :: [SassFunction]+functions =+ [ SassFunction "foo()" fooFunction+ , SassFunction "bar($n)" barFunction+ ]++inclContent :: String+inclContent = "a {\n margin: 1px; }\n"++altInclContent :: String+altInclContent = "b {\n margin: 5px; }\n"++headerFunction :: String -> IO [SassImport]+headerFunction src = return [makeSourceImport $ src ++ "{\n margin: 1px; }\n"]++headers :: [SassHeader]+headers = [SassHeader 1 headerFunction]++importFunction :: String -> String -> IO [SassImport]+importFunction "_imp" _ = return [makeSourceImport inclContent]+importFunction _ _ = return [makeSourceImport altInclContent]++importers :: [SassImporter]+importers = [SassImporter 1 importFunction]++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+ it "should call simple function" $ do+ let opts = def { sassFunctions = Just functions }+ compileString "a { margin: foo(); }" opts `shouldReturn`+ Right "a {\n margin: 1px; }\n"++ it "should correctly pass arguments to function" $ do+ let opts = def { sassFunctions = Just functions }+ compileString "a { margin: bar('a'); }" opts `shouldReturn`+ Right "a {\n margin: 1px; }\n"++ it "should correctly inject header" $ do+ let opts = def { sassHeaders = Just headers, sassInputPath = Just "path" }+ compileString "a { margin : 1px; }" opts `shouldReturn`+ Right ("path {\n margin: 1px; }\n\na {\n margin: 1px; }\n")++ it "should not apply header to imports" $ do+ let opts = def {+ sassHeaders = Just headers+ , sassImporters = Just importers+ , sassInputPath = Just "path"+ }+ compileString "@import '_imp';" opts `shouldReturn`+ Right ("path {\n margin: 1px; }\n\n" ++ inclContent)++ it "should call importers" $ do+ let opts = def { sassImporters = Just importers }+ compileString "@import '_imp';" opts `shouldReturn` Right inclContent++ it "should pass import name to importers" $ do+ let opts = def { sassImporters = Just importers }+ compileString "@import 'other';" opts `shouldReturn` Right altInclContent+
+ test/Text/Sass/TestingUtils.hs view
@@ -0,0 +1,8 @@+module Text.Sass.TestingUtils where++import Test.Hspec++-- | @a `returnShouldSatisfy` p@ asserts that the action @a@ returns value that+-- satisfies predicate @p@.+returnShouldSatisfy :: (Show a, Eq a) => IO a -> (a -> Bool) -> Expectation+v `returnShouldSatisfy` p = v >>= (`shouldSatisfy` p)
+ test/Text/Sass/TutorialSpec.hs view
@@ -0,0 +1,93 @@+module Text.Sass.TutorialSpec where++import Control.Concurrent.MVar+import Test.Hspec+import Text.Sass++max3 :: SassValue -> IO SassValue+max3 (SassList (SassNumber a _:SassNumber b _:SassNumber c _:_) _) =+ return $ SassNumber (max a $ max b c) "px"+max3 _ = return $ SassError "invalid arguments"++max3sig :: SassFunction+max3sig = SassFunction "max3($a, $b, $c)" max3++warn :: MVar String -> SassValue -> IO SassValue+warn m (SassList (SassString s:_) _) = do+ putMVar m s+ return $ SassString "warn!"+warn _ _ = return $ SassString "invalid arguments"++warnSig :: MVar String -> SassFunction+warnSig = SassFunction "@warn" . warn++header :: String -> IO [SassImport]+header src = return [makeSourceImport $ "$file: " ++ src ++ ";"]++headerSig :: SassHeader+headerSig = SassHeader 1 header++importer :: String -> String -> IO [SassImport]+importer imp src = return+ [makeSourceImport $ "/* imported " ++ imp ++ " into " ++ src ++ " */"]++sassImporter :: Maybe [SassImporter]+sassImporter = Just [SassImporter 1 importer]++importer1 :: String -> String -> IO [SassImport]+importer1 imp _ = return [makeSourceImport $ "$file: " ++ imp ++ "1;"]++importer2 :: String -> String -> IO [SassImport]+importer2 imp _ = return [makeSourceImport $ "$file: " ++ imp ++ "2;"]++importerSigs :: [SassImporter]+importerSigs = [SassImporter 0.5 importer1, SassImporter 1 importer2]++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+ describe "max3" $ do+ it "should return max value" $ do+ let vals = SassList [SassNumber 1 "", SassNumber 2 "",+ SassNumber 3 ""] SassSeparatorSpace+ SassNumber v _ <- max3 vals+ v `shouldBe` 3++ it "should return error on invalid aruments" $ do+ let vals = SassList [SassString "", SassString "",+ SassNumber 3 ""] SassSeparatorSpace+ SassError e <- max3 vals+ e `shouldBe` "invalid arguments"++ it "should be usable by compile" $ do+ let opts = def { sassFunctions = Just [max3sig] }+ compileString "foo { margin: max3(1px, 2px, 3px); }" opts+ `shouldReturn` Right "foo {\n margin: 3px; }\n"++ describe "warn" $+ it "should call warn on @warn statement" $ do+ msg <- newEmptyMVar+ let opts = def { sassFunctions = Just [warnSig msg] }+ _ <- compileString "@warn \"message\";" opts :: StringResult+ tryTakeMVar msg `shouldReturn` Just "message"++ describe "Headers" $+ it "should inject header into file" $ do+ let opts = def {+ sassHeaders = Just [headerSig]+ , sassInputPath = Just "path"+ }+ compileString "foo { prop: $file; }" opts `shouldReturn`+ Right "foo {\n prop: path; }\n"++ describe "Importers" $ do+ it "should provide the importer with the correct arguments" $ do+ let opts = def { sassImporters = sassImporter, sassInputPath = Just "file" }+ compileString "@import \"relative/path\"" opts+ `shouldReturn` Right "/* imported relative/path into file */\n"+ it "should inject import with higher priority" $ do+ let opts = def { sassImporters = Just importerSigs }+ compileString "@import \"file\";\nfoo { prop: $file; }" opts+ `shouldReturn` Right "foo {\n prop: file2; }\n"
+ test/Text/Sass/ValuesSpec.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE StandaloneDeriving #-}+module Text.Sass.ValuesSpec where++import Test.Hspec+import Text.Sass+import Text.Sass.Values.Utils++optsWithVal :: SassValue -> SassOptions+optsWithVal v = def {+ sassFunctions = Just [SassFunction "foo()" (\_ -> return v)]+}++testSerialize :: SassValue -> String -> Expectation+testSerialize v e =+ let opts = optsWithVal v+ in compileString "foo { prop: foo(); }" opts `shouldReturn`+ Right ("foo {\n prop: " ++ e ++ "; }\n")++fooFunction :: SassValue -> SassValue -> IO SassValue+fooFunction e (SassList [a] _) = return $ if e == a+ then a+ else SassNull+fooFunction _ _ = error "fooFunction: invalid arguments"++optsWithArg :: SassValue -> SassOptions+optsWithArg a = def {+ sassFunctions = Just [SassFunction "foo($a)" (fooFunction a)]+}++testDeserialize :: SassValue -> String -> Expectation+testDeserialize a e =+ let opts = optsWithArg a+ in compileString ("foo { prop: foo(" ++ e ++ "); }") opts `shouldReturn`+ Right ("foo {\n prop: " ++ e ++ "; }\n")++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+ it "should serialize boolean true" $+ testSerialize (SassBool True) "true"++ it "should serialize boolean false" $+ testSerialize (SassBool False) "false"++ it "should serialize number" $+ testSerialize (SassNumber 1 "px") "1px"++ it "should serialize color" $+ testSerialize (SassColor 1 2 3 0.5) "rgba(1, 2, 3, 0.5)"++ it "should serialize string" $+ testSerialize (SassString "abcd") "abcd"++ it "should serialize list" $+ testSerialize+ (SassList [SassNumber 1 "px", SassNumber 2 "px"]+ SassSeparatorComma)+ "1px, 2px"++ it "should serialize map" $ do+ let sassMap = (SassMap [ (SassString "a", SassString "b")+ , (SassNumber 1 "", SassBool True)])+ let opts = optsWithVal sassMap+ result <- compileString "@each $key, $val in foo() { #{$key} { val: #{$val} } }" opts+ result `shouldBe` Right "a {\n val: b; }\n\n1 {\n val: true; }\n"++ it "should deserialize boolean true" $+ testDeserialize (SassBool True) "true"++ it "should deserialize boolean false" $+ testDeserialize (SassBool False) "false"++ it "should deserialize number" $+ testDeserialize (SassNumber 1 "px") "1px"++ it "should deserialize color" $+ testDeserialize (SassColor 1 2 3 0.5) "rgba(1, 2, 3, 0.5)"++ it "should deserialize string" $+ testDeserialize (SassString "abcd") "abcd"++ it "should deserialize list" $+ testDeserialize+ (SassList [SassNumber 1 "px", SassNumber 2 "px"] SassSeparatorSpace)+ "1px 2px"++ it "should deserialize map" $+ pendingWith "Libsass does not allow maps in CSS any more"++ it "should correctly combine two SassValues" $ do+ let val1 = SassNumber 1.0 ""+ val2 = SassNumber 2.0 ""+ combineSassValues SassAdd val1 val2 `shouldBe` SassNumber 3.0 ""