markdown-unlit-0.6.0/0000755000000000000000000000000007346545000012661 5ustar0000000000000000markdown-unlit-0.6.0/LICENSE0000644000000000000000000000206707346545000013673 0ustar0000000000000000Copyright (c) 2012-2021 Simon Hengel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. markdown-unlit-0.6.0/Setup.lhs0000644000000000000000000000011407346545000014465 0ustar0000000000000000#!/usr/bin/env runhaskell > import Distribution.Simple > main = defaultMain markdown-unlit-0.6.0/driver/0000755000000000000000000000000007346545000014154 5ustar0000000000000000markdown-unlit-0.6.0/driver/Main.hs0000644000000000000000000000024507346545000015375 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} module Main where import System.Environment import Text.Markdown.Unlit main :: IO () main = getArgs >>= run markdown-unlit-0.6.0/markdown-unlit.cabal0000644000000000000000000000334307346545000016623 0ustar0000000000000000cabal-version: 1.12 -- This file has been generated from package.yaml by hpack version 0.35.2. -- -- see: https://github.com/sol/hpack name: markdown-unlit version: 0.6.0 synopsis: Literate Haskell support for Markdown category: Development homepage: https://github.com/sol/markdown-unlit#readme bug-reports: https://github.com/sol/markdown-unlit/issues license: MIT license-file: LICENSE copyright: (c) 2012-2021 Simon Hengel author: Simon Hengel maintainer: Simon Hengel build-type: Simple description: Documentation is here: source-repository head type: git location: https://github.com/sol/markdown-unlit library hs-source-dirs: src ghc-options: -Wall build-depends: base ==4.* , base-compat exposed-modules: Text.Markdown.Unlit other-modules: Paths_markdown_unlit default-language: Haskell2010 executable markdown-unlit main-is: Main.hs other-modules: Paths_markdown_unlit hs-source-dirs: driver ghc-options: -Wall build-depends: base ==4.* , base-compat , markdown-unlit default-language: Haskell2010 test-suite spec type: exitcode-stdio-1.0 main-is: Spec.hs hs-source-dirs: test src ghc-options: -Wall cpp-options: -DTEST build-tool-depends: hspec-discover:hspec-discover build-depends: QuickCheck , base ==4.* , base-compat , directory , hspec ==2.* , silently , stringbuilder , temporary other-modules: Text.Markdown.UnlitSpec Text.Markdown.Unlit Paths_markdown_unlit default-language: Haskell2010 markdown-unlit-0.6.0/src/Text/Markdown/0000755000000000000000000000000007346545000016156 5ustar0000000000000000markdown-unlit-0.6.0/src/Text/Markdown/Unlit.hs0000644000000000000000000001266707346545000017621 0ustar0000000000000000{-# LANGUAGE CPP #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ViewPatterns #-} module Text.Markdown.Unlit ( run , unlit , Selector (..) , parseSelector , CodeBlock (..) , parse #ifdef TEST , parseReorderingKey , parseClasses #endif ) where import Prelude () import Prelude.Compat import Control.Arrow import Data.Char import Data.List.Compat import Data.Maybe import Data.String import System.Environment import System.Exit import System.IO import Text.Read fenceChars :: [Char] fenceChars = ['`', '~'] fences :: [String] fences = map (replicate 3) fenceChars -- | Program entry point. run :: [String] -> IO () run args = -- GHC calls unlit like so: -- -- > unlit [args] -h label Foo.lhs /tmp/somefile -- -- [args] are custom arguments provided with -optL -- -- The label is meant to be used in line pragmas, like so: -- -- #line 1 "label" -- case break (== "-h") args of (mkSelector -> selector, "-h" : files) -> case files of [src, cur, dst] -> do readFileUtf8 cur >>= writeFileUtf8 dst . unlit src selector [src] -> do readFileUtf8 src >>= writeUtf8 stdout . unlit src selector _ -> usage _ -> usage where usage :: IO () usage = do name <- getProgName hPutStrLn stderr ("usage: " ++ name ++ " [selector] -h SRC CUR DST") exitFailure mkSelector :: [String] -> Selector mkSelector = fromMaybe ("haskell" :&: Not "ignore") . parseSelector . unwords readFileUtf8 :: FilePath -> IO String readFileUtf8 name = openFile name ReadMode >>= \ handle -> hSetEncoding handle utf8 >> hGetContents handle writeFileUtf8 :: FilePath -> String -> IO () writeFileUtf8 name str = withFile name WriteMode $ \ handle -> writeUtf8 handle str writeUtf8 :: Handle -> String -> IO () writeUtf8 handle str = hSetEncoding handle utf8 >> hPutStr handle str unlit :: FilePath -> Selector -> String -> String unlit src selector = unlines . concatMap formatCodeBlock . sortCodeBlocks . filter (toPredicate selector . codeBlockClasses) . parse where formatCodeBlock :: CodeBlock -> [String] formatCodeBlock cb = ("#line " ++ show (codeBlockStartLine cb) ++ " " ++ show src) : codeBlockContent cb sortCodeBlocks :: [CodeBlock] -> [CodeBlock] sortCodeBlocks = map fst . sortOn snd . addSortKey where addSortKey :: [CodeBlock] -> [(CodeBlock, (ReorderingKey, DeclarationOrder))] addSortKey = zipWith ((id &&&) . sortKey) [0..] sortKey :: a -> CodeBlock -> (ReorderingKey, a) sortKey n code = (reorderingKey code, n) toPredicate :: Selector -> [String] -> Bool toPredicate = go where go s = case s of Class c -> elem c Not p -> not . go p a :&: b -> (&&) <$> go a <*> go b a :|: b -> (||) <$> go a <*> go b newtype DeclarationOrder = DeclarationOrder Int deriving newtype (Eq, Ord, Enum, Num) newtype ReorderingKey = ReorderingKey Int deriving newtype (Eq, Show, Read, Ord, Bounded, Num) reorderingKey :: CodeBlock -> ReorderingKey reorderingKey = parseReorderingKey . codeBlockClasses parseReorderingKey :: [String] -> ReorderingKey parseReorderingKey = go where go :: [String] -> ReorderingKey go = \ case [] -> 0 "top" : _ -> minBound ('t' : 'o' : 'p' : ':' : (readMaybe -> Just n)) : _ -> minBound + n _ : classes -> go classes infixr 3 :&: infixr 2 :|: data Selector = Class String | Not Selector | Selector :&: Selector | Selector :|: Selector deriving (Eq, Show) parseSelector :: String -> Maybe Selector parseSelector input = case words input of [] -> Nothing xs -> (Just . foldr1 (:|:) . map parseAnds) xs where parseAnds = foldr1 (:&:) . map parseClass . split (== '+') parseClass c = case c of '!':xs -> Not (Class xs) _ -> Class c -- a copy from https://github.com/sol/string split :: (Char -> Bool) -> String -> [String] split p = go where go xs = case break p xs of (ys, []) -> [ys] (ys, _:zs) -> ys : go zs instance IsString Selector where fromString = Class data CodeBlock = CodeBlock { codeBlockClasses :: [String] , codeBlockContent :: [String] , codeBlockStartLine :: Int } deriving (Eq, Show) type Line = (Int, String) parse :: String -> [CodeBlock] parse = go . zip [2..] . lines where go :: [Line] -> [CodeBlock] go xs = case break isFence xs of (_, []) -> [] (_, y:ys) -> case takeCB y ys of (cb, rest) -> cb : go rest takeCB :: Line -> [Line] -> (CodeBlock, [Line]) takeCB (n, fence) xs = let indent = length . takeWhile isSpace $ fence in case break isFence xs of (cb, rest) -> (CodeBlock (parseClasses fence) (map (drop indent . snd) cb) n, drop 1 rest) isFence :: Line -> Bool isFence = p . dropWhile isSpace . snd where p :: String -> Bool p line = any (`isPrefixOf` line) fences parseClasses :: String -> [String] parseClasses xs = words . replace '.' ' ' $ case dropWhile isSpace . dropWhile (`elem` fenceChars) . dropWhile isSpace $ xs of '{':ys -> takeWhile (/= '}') ys ys -> ys replace :: Char -> Char -> String -> String replace x sub = map f where f y | x == y = sub | otherwise = y markdown-unlit-0.6.0/test/0000755000000000000000000000000007346545000013640 5ustar0000000000000000markdown-unlit-0.6.0/test/Spec.hs0000644000000000000000000000005407346545000015065 0ustar0000000000000000{-# OPTIONS_GHC -F -pgmF hspec-discover #-} markdown-unlit-0.6.0/test/Text/Markdown/0000755000000000000000000000000007346545000016346 5ustar0000000000000000markdown-unlit-0.6.0/test/Text/Markdown/UnlitSpec.hs0000644000000000000000000001713707346545000020621 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} module Text.Markdown.UnlitSpec (main, spec) where import Test.Hspec import Test.QuickCheck import Data.String.Builder import System.Environment import Control.Exception import System.Exit import System.IO.Silently import System.IO import System.IO.Temp (withSystemTempFile) import System.Directory import qualified Control.Exception as E import Text.Markdown.Unlit main :: IO () main = hspec spec withTempFile :: (FilePath -> IO ()) -> IO () withTempFile action = withSystemTempFile "hspec" $ \f h -> do hClose h action f `E.finally` removeFile f spec :: Spec spec = do describe "run" $ do it "prints a usage message" $ do withProgName "foo" $ do (r, Left (ExitFailure 1)) <- hCapture [stderr] (try $ run []) r `shouldBe` "usage: foo [selector] -h SRC CUR DST\n" it "unlits code marked with .haskell by default (unless it is marked with .ignore as well)" $ do withTempFile $ \infile -> withTempFile $ \outfile -> do writeFile infile . build $ do "```haskell" "some code" "```" "```haskell ignore" "some other code" "```" run ["-h", "Foo.lhs", infile, outfile] readFile outfile `shouldReturn` (build $ do "#line 2 \"Foo.lhs\"" "some code" ) it "moves code that is marked with .top to the beginning of the file" $ do withTempFile $ \infile -> withTempFile $ \outfile -> do writeFile infile . build $ do "```haskell top" "module Foo where" "```" "" "```haskell" "foo :: Int" "foo = 23" "```" "" "```haskell top" "import Bar" "```" run ["-h", "Foo.lhs", infile, outfile] readFile outfile `shouldReturn` (build $ do "#line 2 \"Foo.lhs\"" "module Foo where" "#line 11 \"Foo.lhs\"" "import Bar" "#line 6 \"Foo.lhs\"" "foo :: Int" "foo = 23" ) it "can be customized" $ do withTempFile $ \infile -> withTempFile $ \outfile -> do writeFile infile . build $ do "```foo" "some code" "" "```" "``` {.bar}" "some other code" "```" run ["bar", "-h", "Foo.lhs", infile, outfile] readFile outfile `shouldReturn` (build $ do "#line 6 \"Foo.lhs\"" "some other code" ) describe "parseReorderingKey" $ do it "returns 0" $ do parseReorderingKey [] `shouldBe` 0 context "with .top" $ do it "returns minBound" $ do parseReorderingKey ["top"] `shouldBe` minBound context "with top:n" $ do it "returns (minBound + n)" $ do parseReorderingKey ["top:20"] `shouldBe` minBound + 20 describe "parseSelector" $ do it "parses + as :&:" $ do parseSelector "foo+bar+baz" `shouldBe` Just ("foo" :&: "bar" :&: "baz") it "parses whitespace as :|:" $ do parseSelector "foo bar baz" `shouldBe` Just ("foo" :|: "bar" :|: "baz") it "parses ! as Not" $ do parseSelector "foo+!bar+baz" `shouldBe` Just ("foo" :&: Not "bar" :&: "baz") it "can handle a combination of :&: and :|:" $ do parseSelector "foo+bar baz+bar" `shouldBe` Just ("foo" :&: "bar" :|: "baz" :&: "bar") it "is total" $ do property $ \xs -> parseSelector xs `seq` True describe "unlit" $ do it "can be used to unlit everything with a specified class" $ do unlit "Foo.lhs" "foo" . build $ do "~~~ {.foo}" "foo" "~~~" "~~~ {.bar}" "bar" "~~~" `shouldBe` (build $ do "#line 2 \"Foo.lhs\"" "foo" ) it "can handle Not" $ do unlit "Foo.lhs" (Not "foo") . build $ do "~~~ {.foo}" "1" "~~~" "~~~ {.bar}" "2" "~~~" `shouldBe` (build $ do "#line 5 \"Foo.lhs\"" "2" ) it "can handle :&:" $ do unlit "Foo.lhs" ("foo" :&: "bar") . build $ do "~~~ {.foo}" "some code" "~~~" "~~~ {.foo .bar}" "some other code" "~~~" `shouldBe` (build $ do "#line 5 \"Foo.lhs\"" "some other code" ) it "can handle :|:" $ do unlit "Foo.lhs" ("foo" :|: "bar") . build $ do "~~~ {.foo}" "foo" "~~~" "~~~ {.bar}" "bar" "~~~" `shouldBe` (build $ do "#line 2 \"Foo.lhs\"" "foo" "#line 5 \"Foo.lhs\"" "bar" ) it "can handle a combination of :&: and :|:" $ do unlit "Foo.lhs" ("foo" :&: "bar" :|: "foo" :&: "baz") . build $ do "~~~ {.foo .bar}" "one" "~~~" "~~~ {.foo .baz}" "two" "~~~" "~~~ {.bar .baz}" "two" "~~~" `shouldBe` (build $ do "#line 2 \"Foo.lhs\"" "one" "#line 5 \"Foo.lhs\"" "two" ) it "can handle a combination of :&: and Not" $ do unlit "Foo.lhs" ("foo" :&: Not "bar" :&: "baz") . build $ do "~~~ {.foo}" "1" "~~~" "~~~ {.foo .bar}" "2" "~~~" "~~~ {.foo .baz}" "3" "~~~" "~~~ {.foo .bar .baz}" "4" "~~~" `shouldBe` (build $ do "#line 8 \"Foo.lhs\"" "3" ) describe "parse" $ do it "yields an empty list on empty input" $ do parse "" `shouldBe` [] it "parses a code block" $ do map codeBlockContent . parse . build $ do "some text" "~~~" "some" "code" "~~~" "some other text" `shouldBe` [["some", "code"]] it "accepts backticks as fence" $ do parse . build $ do "``` {.haskell .ignore}" "some" "code" "```" `shouldBe` [CodeBlock ["haskell", "ignore"] ["some", "code"] 2] it "parses an indented code block" $ do map codeBlockContent . parse . build $ do "1. some text" " ~~~" " some" " code" " ~~~" "2. some other text" `shouldBe` [["some", " code"]] it "parses an empty code block" $ do map codeBlockContent . parse . build $ do "some text" "~~~" "~~~" "some other text" `shouldBe` [[]] it "attaches classes to code blocks" $ do map codeBlockClasses . parse . build $ do "~~~ {.haskell .literate}" "some code" "~~~" `shouldBe` [["haskell", "literate"]] it "attaches classes to indented code blocks" $ do map codeBlockClasses . parse . build $ do "1. some text" " ~~~ {.haskell .literate}" " some code" " ~~~" "2. some other text" `shouldBe` [["haskell", "literate"]] it "attaches source locations to code blocks" $ do map codeBlockStartLine . parse . build $ do "some text" "" "~~~" "some" "code" "~~~" "some other text" `shouldBe` [4] describe "parseClasses" $ do it "drops a leading dot" $ do parseClasses "~~~ {.foo .bar}" `shouldBe` ["foo", "bar"] it "treats dots as whitespace" $ do parseClasses "~~~ {foo.bar. ..}" `shouldBe` ["foo", "bar"] context "without {...}" $ do it "accepts single class" $ do parseClasses "```haskell" `shouldBe` ["haskell"] it "accepts multiple classes" $ do parseClasses "```.haskell .ignore" `shouldBe` ["haskell", "ignore"]