packages feed

hsass 0.3.0 → 0.8.0

raw patch · 20 files changed

Files

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 ""