diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -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.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -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)
diff --git a/lima.cabal b/lima.cabal
new file mode 100644
--- /dev/null
+++ b/lima.cabal
@@ -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
diff --git a/src/Converter.hs b/src/Converter.hs
new file mode 100644
--- /dev/null
+++ b/src/Converter.hs
@@ -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
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -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
