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 +4/−0
- pandoc-include-code.cabal +5/−1
- src/Text/Pandoc/Filter/IncludeCode.hs +88/−49
- src/Text/Pandoc/Filter/Range.hs +8/−4
- test/FilterTest.hs +78/−0
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"+ ]