packages feed

pandoc-include-code 1.2.0.2 → 1.3.0.0

raw patch · 5 files changed

+183/−54 lines, 5 filesdep +hspecdep +hspec-expectationsdep +tasty-hspec

Dependencies added: hspec, hspec-expectations, tasty-hspec

Files

CHANGELOG.md view
@@ -1,5 +1,9 @@ # Changelog +* **1.3.0.0**+  - "Snippet mode" and "range mode" can no longer be used together+  - Using the `numberLines` class sets the `startFrom` attribute for correct+    line numbering * **1.2.0.2**   - Replace bad `Setup.hs` script with default one * **1.2.0.1**
pandoc-include-code.cabal view
@@ -8,7 +8,7 @@ author:              Oskar Wickström maintainer:          Oskar Wickström homepage:	     https://github.com/owickstrom/pandoc-include-code-version:             1.2.0.2+version:             1.3.0.0 cabal-version:       >= 1.8 build-type:          Simple category:            Documentation@@ -43,9 +43,13 @@ test-suite filter-tests     type:            exitcode-stdio-1.0     hs-source-dirs:  test+    other-modules:   FilterTest     main-is:         Driver.hs     build-depends:   base                 >= 4        && < 5                    , pandoc-types         >= 1.12     && <= 1.19                    , pandoc-include-code                    , tasty                    , tasty-hunit+                   , tasty-hspec+                   , hspec+                   , hspec-expectations
src/Text/Pandoc/Filter/IncludeCode.hs view
@@ -7,32 +7,43 @@ {-# LANGUAGE RecordWildCards            #-}  module Text.Pandoc.Filter.IncludeCode-  ( includeCode+  ( InclusionMode(..)+  , InclusionError(..)+  , includeCode+  , includeCode'   ) where #if MIN_VERSION_base(4,8,0)+import           Control.Applicative      ((<|>)) #else import           Control.Applicative import           Data.Monoid #endif- import           Control.Monad.Except import           Control.Monad.Reader+import           Control.Monad.State import           Data.Char                (isSpace) import           Data.HashMap.Strict      (HashMap) import qualified Data.HashMap.Strict      as HM import           Data.List                (isInfixOf)+import           Data.Maybe               (catMaybes) import           Data.Text                (Text) import qualified Data.Text                as Text import qualified Data.Text.IO             as Text import           Text.Pandoc.JSON import           Text.Read                (readMaybe) -import           Text.Pandoc.Filter.Range (Range, mkRange, rangeEnd, rangeStart)+import           Text.Pandoc.Filter.Range (LineNumber, Range, mkRange, rangeEnd,+                                           rangeStart) +data InclusionMode+  = SnippetMode Text+  | RangeMode Range+  | EntireFileMode+  deriving (Show, Eq)+ data InclusionSpec = InclusionSpec   { include :: FilePath-  , snippet :: Maybe Text-  , range   :: Maybe Range+  , mode    :: InclusionMode   , dedent  :: Maybe Int   } @@ -42,42 +53,59 @@   deriving (Show, Eq)  data InclusionError-  = InvalidRange Int-                 Int+  = InvalidRange LineNumber+                 LineNumber   | IncompleteRange MissingRangePart+  | ConflictingModes [InclusionMode]   deriving (Show, Eq) +newtype InclusionState = InclusionState+  { startLineNumber :: Maybe LineNumber+  }+ newtype Inclusion a = Inclusion-  { runInclusion :: ReaderT InclusionSpec (ExceptT InclusionError IO) a+  { runInclusion :: ReaderT InclusionSpec (StateT InclusionState (ExceptT InclusionError IO)) a   } deriving ( Functor              , Applicative              , Monad              , MonadIO              , MonadReader InclusionSpec              , MonadError InclusionError+             , MonadState InclusionState              ) -runInclusion' :: InclusionSpec -> Inclusion a -> IO (Either InclusionError a)-runInclusion' spec action = runExceptT (runReaderT (runInclusion action) spec)+runInclusion' ::+     InclusionSpec+  -> Inclusion a+  -> IO (Either InclusionError (a, InclusionState))+runInclusion' spec action =+  runExceptT (runStateT (runReaderT (runInclusion action) spec) initialState)+  where+    initialState = InclusionState {startLineNumber = Nothing}  parseInclusion ::      HashMap String String -> Either InclusionError (Maybe InclusionSpec) parseInclusion attrs =   case HM.lookup "include" attrs of     Just include -> do-      range <- getRange+      rangeMode <- parseRangeMode+      mode <-+        case catMaybes [rangeMode, snippetMode] of+          []  -> return EntireFileMode+          [m] -> return m+          ms  -> throwError (ConflictingModes ms)       return (Just InclusionSpec {..})     Nothing -> return Nothing   where     lookupInt name = HM.lookup name attrs >>= readMaybe-    snippet = Text.pack <$> HM.lookup "snippet" attrs+    snippetMode = SnippetMode . Text.pack <$> HM.lookup "snippet" attrs     dedent = lookupInt "dedent"-    getRange =+    parseRangeMode =       case (lookupInt "startLine", lookupInt "endLine") of         (Just start, Just end) ->           maybe             (throwError (InvalidRange start end))-            (return . Just)+            (return . Just . RangeMode)             (mkRange start end)         (Nothing, Just _) -> throwError (IncompleteRange Start)         (Just _, Nothing) -> throwError (IncompleteRange End)@@ -85,34 +113,35 @@  type Lines = [Text] +setStartLineNumber :: LineNumber -> Inclusion ()+setStartLineNumber n = modify (\s -> s {startLineNumber = Just n})+ readIncluded :: Inclusion Text readIncluded = liftIO . Text.readFile =<< asks include -filterLineRange :: Lines -> Inclusion Lines-filterLineRange ls =-  asks range >>= \case-    Just range ->-      return (take (rangeEnd range - startIndex) (drop startIndex ls))-      where startIndex = pred (rangeStart range)-    Nothing -> return ls- isSnippetTag :: Text -> Text -> Text -> Bool isSnippetTag tag name line =   mconcat [tag, " snippet ", name] `Text.isSuffixOf` Text.strip line  isSnippetStart, isSnippetEnd :: Text -> Text -> Bool isSnippetStart = isSnippetTag "start"+ isSnippetEnd = isSnippetTag "end" -onlySnippet :: Lines -> Inclusion Lines-onlySnippet ls = do-  s <- asks snippet-  case s of-    Just name ->-      return $-      drop 1 $-      takeWhile (not . isSnippetEnd name) $ dropWhile (not . isSnippetStart name) ls-    Nothing -> return ls+includeByMode :: Lines -> Inclusion Lines+includeByMode ls =+  asks mode >>= \case+    SnippetMode name -> do+      let (before, start) = break (isSnippetStart name) ls+          -- index +1 for line number, then +1 for snippet comment line, so +2:+          startLine = length before + 2+      setStartLineNumber startLine+      return (takeWhile (not . isSnippetEnd name) (drop 1 start))+    RangeMode range -> do+      setStartLineNumber (rangeStart range)+      return (take (rangeEnd range - startIndex) (drop startIndex ls))+      where startIndex = pred (rangeStart range)+    EntireFileMode -> return ls  dedentLines :: Lines -> Inclusion Lines dedentLines ls = do@@ -129,13 +158,20 @@           | otherwise -> Text.cons c cs         Nothing -> "" -filterAttributes :: [(String, String)] -> [(String, String)]-filterAttributes = filter nonFilterAttribute+modifyAttributes ::+     InclusionState -> [String] -> [(String, String)] -> [(String, String)]+modifyAttributes InclusionState {startLineNumber} classes =+  (++) extraAttrs . filter nonFilterAttribute   where     nonFilterAttribute (key, _) = key `notElem` attributeNames     attributeNames = ["include", "startLine", "endLine", "snippet", "dedent"]+    extraAttrs =+      case startLineNumber of+        Just n+          | "numberLines" `elem` classes -> [("startFrom", show n)]+        _ -> [] -printAndFail :: InclusionError -> IO Block+printAndFail :: InclusionError -> IO a printAndFail = fail . formatError   where     formatError =@@ -144,6 +180,7 @@           "Invalid range: " ++ show start ++ " to " ++ show end         IncompleteRange Start -> "Incomplete range: \"startLine\" is missing"         IncompleteRange End -> "Incomplete range: \"endLine\" is missing"+        ConflictingModes modes -> "Conflicting modes: " ++ show modes  splitLines :: Text -> Inclusion Lines splitLines = return . Text.lines@@ -153,22 +190,24 @@  allSteps :: Inclusion Text allSteps =-  readIncluded-  >>= splitLines-  >>= filterLineRange-  >>= onlySnippet-  >>= dedentLines-  >>= joinLines+  readIncluded >>= splitLines >>= includeByMode >>= dedentLines >>= joinLines --- | A Pandoc filter that includes code snippets from external files.-includeCode :: Maybe Format -> Block -> IO Block-includeCode _ cb@(CodeBlock (id', classes, attrs) _) =+includeCode' :: Block -> IO (Either InclusionError Block)+includeCode' cb@(CodeBlock (id', classes, attrs) _) =   case parseInclusion (HM.fromList attrs) of     Right (Just spec) ->       runInclusion' spec allSteps >>= \case-        Left err -> printAndFail err-        Right contents ->-          return (CodeBlock (id', classes, filterAttributes attrs) (Text.unpack contents))-    Right Nothing -> return cb-    Left err -> printAndFail err-includeCode _ x = return x+        Left err -> return (Left err)+        Right (contents, state) ->+          return+            (Right+               (CodeBlock+                  (id', classes, modifyAttributes state classes attrs)+                  (Text.unpack contents)))+    Right Nothing -> return (Right cb)+    Left err -> return (Left err)+includeCode' x = return (Right x)++-- | A Pandoc filter that includes code snippets from external files.+includeCode :: Maybe Format -> Block -> IO Block+includeCode _ = includeCode' >=> either printAndFail return
src/Text/Pandoc/Filter/Range.hs view
@@ -1,16 +1,20 @@ -- | A range that cannot be constructed with incorrect bounds. module Text.Pandoc.Filter.Range-  ( Range+  ( LineNumber+  , Range   , rangeStart   , rangeEnd   , mkRange   ) where -data Range = Range { rangeStart :: Int-                   , rangeEnd   :: Int+type LineNumber = Int++data Range = Range { rangeStart :: LineNumber+                   , rangeEnd   :: LineNumber                    }+  deriving (Show, Eq) -mkRange :: Int -> Int -> Maybe Range+mkRange :: LineNumber -> LineNumber -> Maybe Range mkRange s e   | s > 0 && e > 0 && s <= e = Just (Range s e)   | otherwise = Nothing
+ test/FilterTest.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE TupleSections #-}++module FilterTest where++import Test.Tasty+import Test.Tasty.HUnit++import qualified Text.Pandoc.Filter.IncludeCode as Filter+import Text.Pandoc.JSON++fails a =+  a >>= \case+    Left _ -> return ()+    Right x -> fail ""++a `failsWith` b = a >>= (@?= Left b)++a `succeedsWith` b = a >>= (@?= Right b)++type Formatted = Bool++includeCode ::+     String+  -> [String]+  -> [(String, String)]+  -> IO (Either Filter.InclusionError Block)+includeCode fixtureName classes attrs =+  Filter.includeCode'+    (CodeBlock+       ( ""+       , classes+       , mconcat [[("include", "test/fixtures/" ++ fixtureName)], attrs])+       "")++noRange = (Nothing, Nothing)++codeBlock :: String -> Block+codeBlock = CodeBlock ("", [], [])++tests =+  testGroup+    "Text.Pandoc.Filter.IncludeCode"+    [ testCase "includes snippet" $+      includeCode "foo-snippet.txt" [] [("snippet", "foo")] `succeedsWith`+      codeBlock "foo\n"+    , testCase "includes snippet by exact name, not prefix" $+      includeCode "foo-snippets.txt" [] [("snippet", "foo")] `succeedsWith`+      codeBlock "foo\n"+    , testCase "disallows conflicting modes" $+      fails $+      includeCode+        "foo-snippet.txt"+        []+        [("snippet", "foo"), ("startLine", "2"), ("endLine", "3")]+    , testCase "includes only lines between start and end line" $+      includeCode "some-text.txt" [] [("startLine", "2"), ("endLine", "3")] `succeedsWith`+      codeBlock "world\nthis\n"+    , testCase "dedents lines as much as specified" $+      includeCode "indents.txt" [] [("dedent", "1")] `succeedsWith`+      codeBlock "zero\n two\none\n  three\n       eight\n"+    , testCase+        "dedents lines as much as possible without removing non-whitespace" $+      includeCode "indents.txt" [] [("dedent", "8")] `succeedsWith`+      codeBlock "zero\ntwo\none\nthree\neight\n"+    , testCase "sets startFrom attribute in range mode if numberLines is present" $+      includeCode+        "some-text.txt"+        ["numberLines"]+        [("startLine", "2"), ("endLine", "3")] `succeedsWith`+      CodeBlock ("", ["numberLines"], [("startFrom", "2")]) "world\nthis\n"+    , testCase "sets startFrom attribute in snippet mode if numberLines is present" $+      includeCode+        "foo-snippet.txt"+        ["numberLines"]+        [("snippet", "foo")] `succeedsWith`+      CodeBlock ("", ["numberLines"], [("startFrom", "2")]) "foo\n"+    ]