packages feed

lima (empty) → 0.1.0.0

raw patch · 8 files changed

+710/−0 lines, 8 filesdep +aesondep +basedep +data-defaultsetup-changed

Dependencies added: aeson, base, data-default, lima, optparse-applicative, yaml

Files

+ CHANGELOG.md view
@@ -0,0 +1,9 @@+# Revision history for LiterateMarkdown++## 0.1.0.1 -- 2020-08-29++* code maintainability improvements and update to newer `base` version++## 0.1.0.0 -- 2020-04-11++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2020 Fabian Schneider++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.
+ README.md view
@@ -0,0 +1,116 @@+# lima++Convert between++- `Haskell` (`.hs`) files with `GitHub`-flavoured `Markdown` comments and `Markdown` (`.md`)+- `Literate Haskell` (`.lhs`) files and `GitHub`-flavoured `Markdown` (`.md`).++It is possible to make conversion abide the [roundtrip property](https://jesper.sikanda.be/posts/quickcheck-intro.html). In other words, make conversions `file.lhs` -> `file.lhs.md` -> `file.lhs.md.lhs` or `file.hs` -> `file.hs.md` -> `file.hs.md.hs` and get `file.lhs = file.lhs.md.lhs` and `file.hs = file.hs.md.hs` in terms of their contents.++## Alternatives++- [LiterateMarkdown](https://github.com/haskie-lambda/LiterateMarkdown). `lima` is a fork of this (abandoned?) project. Initially, I just wanted to fix some bugs, but then realized that I can't conveniently use `Haskell Language Server` with `.lhs` files so I added the `.hs` -> `.md` conversion.++- [IHaskell](https://github.com/IHaskell/IHaskell) - create `Jupyter` notebooks with `Haskell` code cells and `GitHub`-flavoured `Markdown` text cells and do much more!++## Conversion++### file.hs -> file.hs.md++[Examples](./testdata/hs)++Rules for `.hs` -> `.md` conversion:++- To produce text blocks, write multiline comments in `GitHub`-flavoured `Markdown`+- Such comments should start with `{- ` or `{-\n`+- Multiline comments (even `{- -}`) split `Haskell` code into snippets+- Special comments like `{- FOURMOLU_ENABLE -}` won't appear in a `.md`. You can supply other comments in a config (`hs-md.special-comments`). See the sample [config](./testdata/config/).+- You can ignore parts of a `.hs` file by enclosing them into `{- LIMA_DISABLE -}` and `{- LIMA_ENABLE -}`. The lines between such comments will be commented out in the resulting `.md`.++### file.lhs -> file.lhs.md -> file.lhs.md.lhs++Examples:++- [input0.lhs](./testdata/input0.lhs) -> [input0.lhs.md](./testdata/input0.lhs.md)+- [input1.lhs](./testdata/input1.lhs) -> [input1.lhs.md](./testdata/input1.lhs.md)++As `.lhs` doesn't support `#` (heading) or `>` (quotation start) at a line beginning, one should write ` #` and ` >` instead.++- ` #` -> `#` -> ` #`+- ` >` -> `>` -> ` >`++If you'd like to provide some code in a `.lhs`, follow these rules:++- `>` is for `Haskell` code, there should be an empty line before and after the block with `Haskell` code+- `<` is for any other code. Such code will be converted into code blocks of type `console` in `.md`+- The round-trip property is not guarranteed if you insert code snippets into `.lhs` using three backticks+  - Nevertheless, feel free to insert them into `.md`. In `.lhs`, they will just lose the language info++## Command-line tool++### cabal++1. Clone this repo and install `lima`.++    ```console+    git clone https://github.com/deemp/flakes+    cd flakes/lima+    cabal update+    cabal install .+    ```++### nix++1. [Install Nix](https://github.com/deemp/flakes/blob/main/README/InstallNix.md)++1. Get `lima` on `PATH`.++    ```console+    nix flake lock github:deemp/flakes+    nix shell github:deemp/flakes?dir=lima+    lima --help+    ```++### Windows++To install the executable on `Windows`, if you can't convince cabal to use [`--bindir-method=copy`](https://github.com/haskell/cabal/issues/5748) you can build the project locally and copy the built executeable to `C:/Users/username/AppData/Roaming/cabal/bin` and ensure that this directory is in your `PATH`.++## Contribute++Clone this repo and enter `lima`++```console+git clone https://github.com/deemp/flakes+cd flakes/lima+```++### cabal++Build as usually++```console+cabal update+cabal build+```++### nix++1. [Install](https://github.com/deemp/flakes/blob/main/README/InstallNix.md) `Nix`. Optionally, [learn](https://github.com/deemp/flakes#prerequisites) more about `Nix`.++1. Run a devshell and build `lima`:++    ```console+    nix develop nix-dev/+    cabal build+    ```++1. Optionally, start `VSCodium`:++    ```console+    nix run nix-dev/#writeSettings+    nix run nix-dev/#codium .+    ```++1. Open a `Haskell` file there, hower over a term and wait until `HLS` shows the hints.++1. [Troubleshoot](https://github.com/deemp/flakes/blob/main/README/Troubleshooting.md) if necessary.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,144 @@+{-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Main (main) where++import Control.Exception (SomeException (SomeException), bracketOnError, catch, throwIO)+import Control.Exception.Base (try)+import Control.Monad (when, zipWithM_, (>=>))+import Converter (Config (..), ConfigHsMd (..), hsToMd, lhsToMd, mdToHs, mdToLhs)+import Data.Aeson.Types (prependFailure)+import Data.Char (toLower)+import Data.Default (def)+import Data.Either (isLeft)+import Data.Foldable (Foldable (..))+import Data.List (intersperse)+import Data.Maybe (fromJust, fromMaybe, isNothing)+import Data.String (IsString)+import Data.Traversable (forM)+import Data.Yaml (FromJSON (..), ParseException, Value (..), withObject, (.:), (.:?))+import Data.Yaml.Aeson (decodeFileEither, withArray)+import Data.Yaml.Parser (typeMismatch)+import GHC.Generics (Generic)+import Options.Applicative+import Options.Applicative.Help (Doc, bold, colon, comma, dot, fill, indent, lparen, nest, rparen, softline, text, (<+>))+import Options.Applicative.Help.Pretty (hardline)+import System.Environment (getArgs)+import System.Exit (ExitCode (..), exitWith)++data CommandType = Hs2Md | Md2Hs | Lhs2Md | Md2Lhs deriving (Show)++data Options = Options+  { commandType :: CommandType+  , config :: Maybe FilePath+  , files :: [FilePath]+  }+  deriving (Show)++-- TODO make config and --file arguments mutually exclusive++parseConfig :: Parser ([FilePath], Maybe FilePath)+parseConfig = do+  file <- some $ strOption (long "file" <> short 'f' <> metavar "FILE" <> help "Path to a file to convert")+  config <- optional (strOption (long "config" <> short 'c' <> metavar "FILE" <> help "Path to a config"))+  return (file, config)++mkConfigParser :: String -> CommandType -> Doc -> Mod CommandFields Options+mkConfigParser command_ commandType progDesc_ =+  command+    command_+    ( info+        ((\(files, config) -> Options{..}) <$> parseConfig)+        (progDescDoc $ Just progDesc_)+    )++uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d+uncurry3 f (a, b, c) = f a b c++lima :: Parser Options+lima =+  subparser $+    foldMap+      (uncurry3 mkConfigParser)+      [ ("hs2md", Hs2Md, "Convert" <-> bold "Haskell" <-> "to" <-> bold "Markdown")+      , ("md2hs", Md2Hs, "Convert" <-> bold "Markdown" <-> "to" <-> bold "Haskell")+      , ("md2lhs", Md2Lhs, "Convert" <-> bold "Markdown" <-> "to" <-> bold "Literate Haskell")+      , ("lhs2md", Lhs2Md, "Convert" <-> bold "Literate Haskell" <-> "to" <-> bold "Markdown")+      ]++maybe' :: Maybe a -> b -> (a -> b) -> b+maybe' x f g = maybe f g x++either' :: Either a b -> (a -> c) -> (b -> c) -> c+either' x f g = either f g x++descriptionBlock :: [Doc] -> Doc+descriptionBlock desc = fold (intersperse softline desc) <> hardline++(<->) :: Doc -> Doc -> Doc+x <-> y = x <> softline <> y++fill' :: Doc+fill' = fill 100 $ text ""++header_ :: Doc+header_ =+  descriptionBlock+    [ bold "lima" <-> "converts between" <-> bold "Haskell" <> lparen <> bold ".hs" <> rparen+    , "and" <-> bold "Markdown" <> lparen <> bold ".md" <> rparen+    , "and between" <-> bold "Literate Haskell" <> lparen <> bold ".lhs" <> rparen+    , "and" <-> bold "Markdown" <> lparen <> bold ".md" <> rparen <> dot+    , fill'+    , "Learn more about a command by running:" <-> bold "lima COMMAND"+    , fill'+    , "Example usage:" <> fill'+    , fill'+    , bold "lima hs2md -f file.hs -c config.yaml" <> fill'+    , indent 2 $ "Convert" <+> bold "HS" <+> "to" <+> bold "Markdown" <+> "using the config" <+> bold "config.yaml" <> colon <> fill'+    , indent 2 "file1.hs -> file1.hs.md"+    , fill'+    , bold "lima lhs2md -f file1.lhs -f file2.lhs" <> fill'+    , indent 2 $ "Convert" <+> bold "LHS" <+> "to" <+> bold "Markdown" <> colon <> fill'+    , indent 2 $ "file1.lhs -> file1.lhs.md" <> comma <-> "file2.lhs -> file2.lhs.md"+    ]++runLima :: IO Options+runLima =+  customExecParser+    (prefs (showHelpOnError <> disambiguate))+    ( info+        (helper <*> lima)+        ( fullDesc <> headerDoc (Just header_)+        )+    )++main :: IO ()+main = do+  Options{..} <- runLima+  Config{..} <-+    maybe'+      config+      def+      ( \c ->+          decodeFileEither c+            >>= ( \(x :: Either ParseException Config) ->+                    either' x (const $ error $ "Could not parse the config file at " <> c) pure+                )+      )+  contents_ <- forM files (\file -> readFile file `catch` (\(x :: SomeException) -> error $ "Could not read file at " <> file))+  let+    convert :: String -> FilePath -> IO ()+    convert contents out =+      (\(f, ext) -> writeFile (out <> "." <> ext) (f contents)) $+        case commandType of+          Md2Hs -> (mdToHs (fromMaybe def configHsMd), "md")+          Hs2Md -> (hsToMd (fromMaybe def configHsMd), "md")+          Md2Lhs -> (mdToLhs, "lhs")+          Lhs2Md -> (lhsToMd, "md")+  zipWithM_ convert contents_ files+  putStrLn "Converted!"+ where+  opts = info (lima <**> helper) mempty+  p = prefs (disambiguate <> showHelpOnError)
+ lima.cabal view
@@ -0,0 +1,68 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.35.0.+--+-- see: https://github.com/sol/hpack++name:           lima+version:        0.1.0.0+synopsis:       Haskell -> Markdown and Literate Haskell <-> Markdown converter+description:    See README here or on [GitHub](https://github.com/deemp/flakes/tree/main/lima#readme)+category:       Productivity+bug-reports:    https://github.com/deemp/flakes/issues+author:         Fabian Schneider+maintainer:     Danila Danko+license:        MIT+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    CHANGELOG.md+    README.md++source-repository head+  type: git+  location: https://github.com/deemp/flakes/tree/main/lima#readme++library+  exposed-modules:+      Converter+  other-modules:+      Paths_lima+  hs-source-dirs:+      src+  build-depends:+      aeson+    , base ==4.*+    , data-default+    , optparse-applicative+    , yaml+  default-language: Haskell2010++executable lima+  main-is: app/Main.hs+  other-modules:+      Paths_lima+  build-depends:+      aeson+    , base ==4.*+    , data-default+    , lima+    , optparse-applicative+    , yaml+  default-language: Haskell2010++test-suite lima-test+  type: exitcode-stdio-1.0+  main-is: Main.hs+  other-modules:+      Paths_lima+  hs-source-dirs:+      test+  build-depends:+      aeson+    , base ==4.*+    , data-default+    , lima+    , optparse-applicative+    , yaml+  default-language: Haskell2010
+ src/Converter.hs view
@@ -0,0 +1,303 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}++{-# HLINT ignore "Redundant bracket" #-}++-- | Functions to convert @Haskell@ to @Markdown@ and between @Literate Haskell@ (@.lhs@) and @Markdown@.+module Converter (hsToMd, mdToHs, lhsToMd, mdToLhs, Config (..), ConfigHsMd (..)) where++import Data.Default (Default)+import Data.List (isPrefixOf, isSuffixOf)+import Data.Yaml (FromJSON (..))+import Data.Yaml.Aeson (withObject, (.:), (.:?))+import GHC.Generics (Generic)++-- | App config.+newtype Config = Config {configHsMd :: Maybe ConfigHsMd} deriving (Generic, Default)++instance FromJSON Config where+  parseJSON = withObject "Configs" (\v -> Config <$> v .:? "hs-md")++-- | Config for @Haskell@ to @Markdown@ converter.+newtype ConfigHsMd = ConfigHs2Md {specialComments :: [String]} deriving (Generic, Default)++instance FromJSON ConfigHsMd where+  parseJSON =+    withObject+      "CommentsToIgnore"+      (\v -> ConfigHs2Md <$> v .: "special-comments")++backticks :: String+backticks = "```"++haskellSnippet :: String+haskellSnippet = backticks ++ "haskell"++consoleSnippet :: String+consoleSnippet = backticks ++ "console"++chooseSnippetType :: String -> String+chooseSnippetType s+  | s `startsWith` birdTrack = haskellSnippet+  | otherwise = consoleSnippet++birdTrack :: String+birdTrack = "> "++reverseBirdTrack :: String+reverseBirdTrack = "< "++birdTracks :: [String]+birdTracks = [birdTrack, reverseBirdTrack]++-- | Convert @Literate Haskell@ to @Markdown@.+--+-- Convert @LHS@ birdtick style to @Markdown@, replacing the code marked by birdticks with @```haskell ... ```@.+lhsToMd :: String -> String+lhsToMd = unlines . convert "" . lines+ where+  convert :: String -> [String] -> [String]+  convert prev []+    | -- close code tags at the end+      prev `startsWithAnyOf` birdTracks =+        [backticks]+    | otherwise = []+  convert prev (h : t)+    | -- check for Haskell code to start+      -- insert newline above code block if needed+      not (prev `startsWithAnyOf` birdTracks)+        && (h `startsWithAnyOf` birdTracks) =+        (["" | prev /= ""]) ++ [chooseSnippetType h, drop 2 h] ++ rest+    | -- check for code+      h `startsWithAnyOf` birdTracks =+        drop 2 h : rest+    | -- check for code end, insert newline after code block if needed+      prev `startsWithAnyOf` birdTracks =+        [backticks] ++ (["" | h /= ""]) ++ [h] ++ rest+    | h `startsWith` (' ' : birdTrack) =+        drop 1 h : rest+    | otherwise = shiftIfHeader h ++ rest+   where+    rest = convert h t++-- | Convert @Markdown@ file to @Literate Haskell@.+--+-- Replace code marked with @```haskell ...```@ with birdticks (@>@)+-- and code marked with @``` ... ```@ with reverse birdticks (@<@).+mdToLhs :: String -> String+mdToLhs = unlines . convert False False "" . lines+ where+  convert :: Bool -> Bool -> String -> [String] -> [String]+  convert inHsCode inSample prev [] = []+  convert inHsCode inSample prev (h : t)+    | -- handle code block starts, add newline if needed+      h == haskellSnippet =+        (["" | prev /= ""]) ++ convert True False prev t+    | -- handle code+      inHsCode && h /= backticks =+        ("> " ++ h) : convert True False h t+    | -- handle code sample+      inSample && h /= backticks =+        ("< " ++ h) : convert False True h t+    | -- move headings one space to right+      isHeading h =+        (' ' : h) : rest+    | -- handle code and sample block ends+      (inHsCode || inSample)+        && h == backticks =+        (["" | null t || head t /= ""])+          ++ convert False False prev t+    | -- handle sample block starts, add newline if needed+      take 3 h == backticks =+        (["" | prev /= ""]) ++ convert False True prev t+    | -- handle quotes+      h `startsWith` birdTrack =+        (" >" ++ drop 1 h) : rest+    | otherwise = h : rest+   where+    -- count headings+    isHeading h = not (null (takeWhile (== '#') h))+    rest = convert False False h t++startsWith :: String -> String -> Bool+startsWith = flip isPrefixOf++startsWithAnyOf :: String -> [String] -> Bool+startsWithAnyOf l = any (startsWith l)++shiftIfHeader :: String -> [String]+shiftIfHeader "" = [""]+shiftIfHeader (' ' : '#' : x) = ['#' : x]+shiftIfHeader x = [x]++_LIMA_DISABLE :: String+_LIMA_DISABLE = "LIMA_DISABLE"++_LIMA_ENABLE :: String+_LIMA_ENABLE = "LIMA_ENABLE"++-- | Comments that should be ignored for some reason+--+-- FOURMOLU_DISABLE is ignored because it's a special comment and shouldn't be visible in a `.md`+specialCommentsDefault :: [String]+specialCommentsDefault = ["FOURMOLU_DISABLE", "FOURMOLU_ENABLE"]++endsWith :: String -> String -> Bool+endsWith = flip isSuffixOf++-- multi-line comments+mcOpen :: String+mcOpen = "{-"++mcOpenSpace :: String+mcOpenSpace = mcOpen ++ " "++mcClose :: String+mcClose = "-}"++mcCloseSpace :: String+mcCloseSpace = " -}"++dropEnd :: Int -> String -> String+dropEnd n s = reverse (drop n (reverse s))++backticksHs :: String+backticksHs = backticks ++ "haskell"++dropEmpties :: [String] -> [String]+dropEmpties = dropWhile (== "")++-- Markdown comments++-- | Open a @Markdown@ comment+mdcOpen :: String+mdcOpen = "<!--"++-- | Close a @Markdown@ comment+mdcClose :: String+mdcClose = "-->"++mdcOpenSpace :: String+mdcOpenSpace = mdcOpen ++ " "++mdcCloseSpace :: String+mdcCloseSpace = " " ++ mdcClose++stripMc :: String -> String+stripMc x = dropEnd (length mcCloseSpace) (drop (length mcOpenSpace) x)++-- | Convert @Haskell@ to @Markdown@.+--+-- Multi-line comments are copied as text blocks and @Haskell@ code is copied as @Haskell@ snippets.+hsToMd :: ConfigHsMd -> String -> String+hsToMd ConfigHs2Md{..} = unlines . dropWhile (== "") . reverse . (\x -> convert True False False x []) . lines+ where+  specialComments_ = specialCommentsDefault ++ specialComments+  convert :: Bool -> Bool -> Bool -> [String] -> [String] -> [String]+  convert inLimaEnable inComments inSnippet (h : hs) res+    | not inComments =+        if+            | -- disable+              -- split a snippet+              h `startsWith` (mcOpenSpace ++ _LIMA_DISABLE) ->+                (convert False False False hs)+                  ([mdcOpenSpace ++ _LIMA_DISABLE] ++ [backticks | inSnippet] ++ res)+            | -- enable+              h `startsWith` (mcOpenSpace ++ _LIMA_ENABLE) ->+                convert True False inSnippet hs ((_LIMA_ENABLE ++ mdcCloseSpace) : res)+            | -- if disabled+              not inLimaEnable ->+                convert inLimaEnable False False hs (h : res)+            | -- a special comment+              -- comment should be in multi-line style like '{- FOURMOLU_DISABLE -}'+              -- splits a snippet+              h `startsWithAnyOf` ((mcOpenSpace ++) <$> specialComments_) ->+                (convert inLimaEnable False False hs)+                  ([mdcOpenSpace ++ stripMc h ++ mdcCloseSpace] ++ ["" | inSnippet] ++ [backticks | inSnippet] ++ dropEmpties res)+            | -- start of a multi-line comment+              -- it should be either like '{- ...' or '{-\n'+              (h `startsWith` mcOpenSpace || h == mcOpen) ->+                let x' = drop 3 h+                    pref = "" : [backticks | inSnippet]+                    res' = if inSnippet then dropEmpties res else res+                 in -- if a multiline comment ends on the same line+                    -- it should end with '-}'+                    if h `endsWith` mcClose+                      then convert inLimaEnable False False hs ([dropEnd (length mcCloseSpace) x'] ++ pref ++ res')+                      else convert inLimaEnable True False hs ([x' | not (null x')] ++ pref ++ res')+            | -- non-empty line means the start of a Haskell snippet+              not inSnippet ->+                if not (null h)+                  then -- non-empty line means the start of a Haskell snippet+                    convert inLimaEnable False True hs ([h, backticksHs] ++ squashEmpties res)+                  else -- if not in snippet, collapse consequent empty lines+                    convert inLimaEnable False False hs res+            | inSnippet ->+                convert inLimaEnable False True hs (h : res)+    | inComments =+        if -- end of a multiline comment+        h `startsWith` mcClose+          then convert inLimaEnable False False hs res+          else -- copy everything from comments+            convert inLimaEnable True False hs (h : res)+  convert _ _ inSnippet [] res =+    [backticks | inSnippet] ++ dropEmpties res++stripMdc :: String -> String+stripMdc x = dropEnd (length mdcCloseSpace) (drop (length mdcOpenSpace) x)++squashEmpties :: [String] -> [String]+squashEmpties = ([""] ++) . dropEmpties++-- | Convert @Markdown@ to @Haskell@.+--+-- Multi-line comments are copied as text blocks and @Haskell@ code is copied as @Haskell@ snippets.+mdToHs :: ConfigHsMd -> String -> String+mdToHs ConfigHs2Md{..} = unlines . dropWhile (== "") . reverse . (\x -> convert False False False x []) . lines+ where+  specialComments_ = specialCommentsDefault ++ specialComments+  closeTextIf x = [mcClose | x]+  convert :: Bool -> Bool -> Bool -> [String] -> [String] -> [String]+  convert inText inSnippet inComments (h : hs) res+    | inComments =+        if -- enable+        h `startsWith` (_LIMA_ENABLE ++ mdcCloseSpace)+          then -- split text++            (convert inText inSnippet False hs)+              ([mcOpenSpace ++ _LIMA_ENABLE ++ mcCloseSpace] ++ ["" | inText] ++ closeTextIf inText ++ res)+          else -- in a comment+            convert False False True hs (h : res)+    | not inSnippet =+        if+            | -- found comments+              h `startsWith` (mdcOpenSpace ++ _LIMA_DISABLE) ->+                (convert False False True hs)+                  ([mcOpenSpace ++ _LIMA_DISABLE ++ mcCloseSpace] ++ ["" | inText] ++ closeTextIf inText ++ res)+            | -- found a special comment+              h `startsWithAnyOf` ((mdcOpenSpace ++) <$> specialComments_) ->+                (convert False False False hs)+                  ([mcOpenSpace ++ stripMdc h ++ mcCloseSpace] ++ ["" | inText] ++ closeTextIf inText ++ res)+            | -- start of a haskell snippet+              h `startsWith` backticksHs ->+                convert False True False hs (closeTextIf inText ++ squashEmpties res)+            | not inText ->+                if null h+                  then -- just a blank line+                    convert inText False False hs res+                  else -- start of text+                    convert True False False hs ([h, mcOpen, ""] ++ res)+            | -- copy text line by line+              otherwise ->+                convert True False False hs (h : res)+    | otherwise =+        if h == backticks+          then -- end of a snippet+            convert False False False hs res+          else -- in a snippet+            convert False True False hs (h : res)+  convert inText _ _ [] res = [mcClose | inText] ++ res
+ test/Main.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Main (main) where++import Control.Exception (SomeException)+import Control.Exception.Base (catch)+import Control.Monad (when, zipWithM_)+import Converter (Config (..), hsToMd, lhsToMd, mdToHs, mdToLhs)+import Data.Char (toLower)+import Data.Default (Default (..))+import Data.Maybe (fromMaybe)+import Data.Yaml (decodeFileEither, decodeFileThrow)+import System.Environment (getArgs)+import System.Exit (ExitCode (..), exitWith)++testDir :: String+testDir = "testdata/"++main :: IO ()+main = do+  -- test round-trip btw hs and md+  let pathsHs1 = ((testDir ++ "/hs/") ++) <$> ["input0.hs"]+      pathsMd1 = (++ ".md") <$> pathsHs1+      pathsHs2 = (++ ".hs") <$> pathsMd1+  Config{..} <-+    decodeFileThrow (testDir ++ "/config/lima.yaml")+      `catch` (\(x :: SomeException) -> exitWith $ ExitFailure 1)+  contentsHs1 <- mapM readFile pathsHs1+  let contentsMd1 = hsToMd (fromMaybe def configHsMd) <$> contentsHs1+      contentsHs2 = mdToHs (fromMaybe def configHsMd) <$> contentsMd1+  zipWithM_ writeFile pathsMd1 contentsMd1+  zipWithM_ writeFile pathsHs2 contentsHs2++  -- test round-trip btw lhs and md+  let pathsLhs1 = ((testDir ++ "/lhs/") ++) <$> ["input0.lhs", "input1.lhs"]+      pathsMd1 = (++ ".md") <$> pathsLhs1+      pathsLhs2 = (++ ".lhs") <$> pathsMd1+  contentsLhs1 <- mapM readFile pathsLhs1+  let contentsMd1 = lhsToMd <$> contentsLhs1+      contentsLhs2 = mdToLhs <$> contentsMd1+  zipWithM_ writeFile pathsMd1 contentsMd1+  zipWithM_ writeFile pathsLhs2 contentsLhs2++  exitWith $+    if contentsLhs1 == contentsLhs2 && contentsHs1 == contentsHs2+      then ExitSuccess+      else ExitFailure 1