packages feed

lima 0.1.0.6 → 0.2.0.0

raw patch · 30 files changed

+2527/−2415 lines, 30 filesdep +breakpointdep +directorydep +doctest-paralleldep −aesondep −optparse-applicativedep −yamlnew-component:exe:readmePVP ok

version bump matches the API change (PVP)

Dependencies added: breakpoint, directory, doctest-parallel, hedgehog, microlens, microlens-th, pretty-simple, tasty, tasty-hedgehog, tasty-hunit, text

Dependencies removed: aeson, optparse-applicative, yaml

API changes (from Hackage documentation)

- Converter: ConfigHs2Md :: [String] -> ConfigHsMd
- Converter: [configHsMd] :: Config -> Maybe ConfigHsMd
- Converter: [specialComments] :: ConfigHsMd -> [String]
- Converter: hsToMd :: ConfigHsMd -> String -> String
- Converter: instance Data.Aeson.Types.FromJSON.FromJSON Converter.Config
- Converter: instance Data.Aeson.Types.FromJSON.FromJSON Converter.ConfigHsMd
- Converter: instance Data.Default.Class.Default Converter.Config
- Converter: instance Data.Default.Class.Default Converter.ConfigHsMd
- Converter: instance GHC.Generics.Generic Converter.Config
- Converter: instance GHC.Generics.Generic Converter.ConfigHsMd
- Converter: lhsToMd :: String -> String
- Converter: mdToHs :: ConfigHsMd -> String -> String
- Converter: mdToLhs :: String -> String
- Converter: newtype Config
- Converter: newtype ConfigHsMd
+ Converter: (&) :: a -> (a -> b) -> b
+ Converter: (?~) :: ASetter s t a (Maybe b) -> b -> s -> t
+ Converter: Comment :: NonEmpty Text -> Token
+ Converter: Dedent :: Token
+ Converter: Disabled :: [Text] -> Token
+ Converter: HaskellCode :: [Text] -> Token
+ Converter: Hs :: Format
+ Converter: Indent :: Int -> Token
+ Converter: Lhs :: Format
+ Converter: Md :: Format
+ Converter: TeX :: Format
+ Converter: Text :: NonEmpty Text -> Token
+ Converter: [$sel:_dedent:Config] :: Config (a :: Mode') -> Mode a Text
+ Converter: [$sel:_disable:Config] :: Config (a :: Mode') -> Mode a Text
+ Converter: [$sel:_enable:Config] :: Config (a :: Mode') -> Mode a Text
+ Converter: [$sel:_indent:Config] :: Config (a :: Mode') -> Mode a Text
+ Converter: [$sel:_mdHaskellCodeEnd:Config] :: Config (a :: Mode') -> Mode a Text
+ Converter: [$sel:_mdHaskellCodeStart:Config] :: Config (a :: Mode') -> Mode a Text
+ Converter: [$sel:_texHaskellCodeEnd:Config] :: Config (a :: Mode') -> Mode a Text
+ Converter: [$sel:_texHaskellCodeStart:Config] :: Config (a :: Mode') -> Mode a Text
+ Converter: [$sel:manyLines:Indent] :: Token -> [Text]
+ Converter: [$sel:n:Indent] :: Token -> Int
+ Converter: [$sel:someLines:Indent] :: Token -> NonEmpty Text
+ Converter: class Show a => PrettyPrint a
+ Converter: convertTo :: Format -> Format -> Config User -> Text -> Text
+ Converter: data Config (a :: Mode')
+ Converter: data Format
+ Converter: data Token
+ Converter: dedent :: forall a_a70q. Lens' (Config a_a70q) (Mode a_a70q Text)
+ Converter: def :: Default a => a
+ Converter: disable :: forall a_a70q. Lens' (Config a_a70q) (Mode a_a70q Text)
+ Converter: enable :: forall a_a70q. Lens' (Config a_a70q) (Mode a_a70q Text)
+ Converter: exampleNonTexTokens :: Tokens
+ Converter: exampleNonTexTokens' :: Tokens
+ Converter: exampleTexTokens :: Tokens
+ Converter: fromInternalConfig :: Config Internal -> Config User
+ Converter: hsFromTokens :: Config User -> Tokens -> Text
+ Converter: hsFromTokens' :: Config User -> Tokens -> [Text]
+ Converter: hsToTokens :: Config User -> Text -> Tokens
+ Converter: indent :: forall a_a70q. Lens' (Config a_a70q) (Mode a_a70q Text)
+ Converter: infixl 1 &
+ Converter: infixr 4 ?~
+ Converter: instance Converter.PrettyPrint (Converter.Config Converter.User)
+ Converter: instance Converter.PrettyPrint Converter.Tokens
+ Converter: instance Converter.PrettyPrint Data.Text.Internal.Text
+ Converter: instance Converter.PrettyPrint GHC.Base.String
+ Converter: instance Data.Data.Data Converter.Token
+ Converter: instance Data.Default.Class.Default (Converter.Config Converter.Internal)
+ Converter: instance Data.Default.Class.Default (Converter.Config Converter.User)
+ Converter: instance Data.Default.Class.Default Converter.State
+ Converter: instance GHC.Classes.Eq (Converter.Config Converter.User)
+ Converter: instance GHC.Classes.Eq Converter.Token
+ Converter: instance GHC.Generics.Generic (Converter.Config a)
+ Converter: instance GHC.Generics.Generic Converter.State
+ Converter: instance GHC.Show.Show (Converter.Config Converter.Internal)
+ Converter: instance GHC.Show.Show (Converter.Config Converter.User)
+ Converter: instance GHC.Show.Show Converter.Token
+ Converter: instance GHC.Show.Show a => GHC.Show.Show (Converter.Pretty a)
+ Converter: lhsFromTokens :: Config User -> Tokens -> Text
+ Converter: lhsFromTokens' :: Config User -> Tokens -> [Text]
+ Converter: lhsToTokens :: Config User -> Text -> Tokens
+ Converter: mdFromTokens :: Config User -> Tokens -> Text
+ Converter: mdFromTokens' :: Config User -> Tokens -> [Text]
+ Converter: mdHaskellCodeEnd :: forall a_a70q. Lens' (Config a_a70q) (Mode a_a70q Text)
+ Converter: mdHaskellCodeStart :: forall a_a70q. Lens' (Config a_a70q) (Mode a_a70q Text)
+ Converter: mdToTokens :: Config User -> Text -> Tokens
+ Converter: mergeTokens :: Tokens -> Tokens
+ Converter: normalizeTokens :: Tokens -> Tokens
+ Converter: pp :: PrettyPrint a => a -> Pretty String
+ Converter: selectFromTokens :: Config User -> Format -> Tokens -> Text
+ Converter: selectToTokens :: Config User -> Format -> Text -> Tokens
+ Converter: showFormatExtension :: Format -> String
+ Converter: showFormatName :: Format -> String
+ Converter: stripEmpties :: [Text] -> [Text]
+ Converter: stripTokens :: Tokens -> Tokens
+ Converter: texFromTokens :: Config User -> Tokens -> Text
+ Converter: texFromTokens' :: Config User -> Tokens -> [Text]
+ Converter: texHaskellCodeEnd :: forall a_a70q. Lens' (Config a_a70q) (Mode a_a70q Text)
+ Converter: texHaskellCodeStart :: forall a_a70q. Lens' (Config a_a70q) (Mode a_a70q Text)
+ Converter: texToTokens :: Config User -> Text -> Tokens
+ Converter: toInternalConfig :: Config User -> Config Internal
+ Converter: type Internal = 'Internal
+ Converter: type Tokens = [Token]
+ Converter: type User = 'User
+ Converter: type family Mode a b
- Converter: Config :: Maybe ConfigHsMd -> Config
+ Converter: Config :: Mode a Text -> Mode a Text -> Mode a Text -> Mode a Text -> Mode a Text -> Mode a Text -> Mode a Text -> Mode a Text -> Config (a :: Mode')

Files

+ README.hs view
@@ -0,0 +1,127 @@+{-+# lima++Convert between files in different formats.++## Related works++- [LiterateMarkdown](https://github.com/haskie-lambda/LiterateMarkdown) - `lima` is a fork of this (abandoned?) project.++- [pandoc](https://www.uv.es/wikibase/doc/cas/pandoc_manual_instalado.wiki?60) - [supports](https://www.uv.es/wikibase/doc/cas/pandoc_manual_instalado.wiki?60) `Literate Haskell` and a ton of other formats.++- [IHaskell](https://github.com/IHaskell/IHaskell) - create `Jupyter` notebooks with `Haskell` code cells and `GitHub Flavored Markdown` text cells and do much more.++- [lhs2tex](https://hackage.haskell.org/package/lhs2tex) - convert `Literate Haskell` to `TeX`.++## Supported formats++- `Haskell` (`.hs`)+- `Literate Haskell` (`.lhs`)+- `GitHub Flavored Markdown` (`.md`)+- `TeX` (`.tex`)++## Demo++### Markdown++[.hs](./testdata/md/test.hs) and [.md](./testdata/md/test.md)++![demo](https://raw.githubusercontent.com/deemp/lima/main/README/md-demo-hs-md-preview.png)++### TeX++[.hs](./testdata/tex/test.hs) and [.lhs](./testdata/tex/test.lhs) and [.tex](./testdata/tex/test.tex)++![demo](https://raw.githubusercontent.com/deemp/lima/main/README/tex-demo-hs-lhs-tex.png)++## Ideas++- I introduced tags into supported formats.+  - E.g., in `.hs` files, tags are multiline comments written on a single line like `{- LIMA_ENABLE -}`.+- Tag names are configurable.+  - A user may set `on` instead of `LIMA_ENABLE`.+- A document can be parsed into a list of tokens.+  - Tags affect how a document is parsed.+- The tokens can be printed back to that document.+- After printing a parsed document back to itself, the document becomes formatted.+  - Formatting is idempotent. In other words, formatting the document again won't change its contents.+- The `lima` library provides a parser and a printer for each supported format.+- A composition of a printer after a parser produces a converter.+- Such a converter is usually invertible for two formatted documents.+  - `TeX` format requires special tags surrounding the `Haskell` code blocks for this property to hold.++## Setup++1. Create a test suite.+1. Add `lima` and `text` to its dependencies.+1. Create a test module. It can have the following contents.+-}++{- LIMA_INDENT 4 -}++import Converter (Format (..), convertTo, def)+import Data.Text.IO qualified as T++main :: IO ()+main = T.readFile "README.hs" >>= T.writeFile "README.md" . (Hs `convertTo` Md) def++{- LIMA_DEDENT -}++{-+### Example++This package has two such test suites:++- [readme-hs-to-md](test/HsToMd/Main.hs) converts `README.hs` to `README.md`+- [readme-md-to-hs](test/MdToHs/Main.hs) converts `README.md` to `README.hs`++## Workflow++Here's a possible workflow for `Haskell` and `Markdown`:++1. Edit the code in a `README.hs` using [Haskell Language Server](https://github.com/haskell/haskell-language-server).+1. Convert `README.hs` to a `README.md`. Comments from `README.hs` become text in `README.md`.+1. Edit the text in `README.md` using [markdownlint](https://github.com/DavidAnson/markdownlint).+1. Convert `README.md` back to the `README.hs` to keep files in sync. Text in `README.md` becomes comments in `README.hs`.+1. Repeat.++## Contribute++Clone this repo and enter `lima`++```console+git clone https://github.com/deemp/lima+cd lima+```++### cabal++Build++```console+cabal update+cabal build+```++### nix++1. [Install](https://github.com/deemp/flakes/blob/main/README/InstallNix.md) `Nix`.++1. Run a devshell and build `lima` using the project's `cabal`:++    ```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, hover over a term and wait until `HLS` shows hints.++1. [Troubleshoot](https://github.com/deemp/flakes/blob/main/README/Troubleshooting.md) if necessary.+-}
README.md view
@@ -1,158 +1,101 @@ # lima -Convert between--- `Haskell` (`.hs`) and `GitHub Flavored Markdown` (`.md`)-- `Literate Haskell` (`.lhs`) and `GitHub Flavored Markdown` (`.md`).--It is usually 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.--## Demo--![demo](https://github.com/deemp/flakes/blob/main/lima/README/demo.png?raw=true)--## 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 Flavored Markdown` text cells and do much more!--## Conversion--### .hs -> .md--#### Examples--1. [hs](./testdata/hs/input0.hs) -> [hs.md](./testdata/hs/input0.hs.md) -> [hs.md.hs](./testdata/hs/input0.hs.md.hs)+Convert between files in different formats. -#### Rules+## Related works -- **Multiline comments**:-  - Become text blocks.-  - Should be written:-    - in `GitHub Flavored Markdown`;-    - on one line: `{- <COMMENTS> -}`;-    - on many lines: `{-\n<COMMENTS>\n-}`.-  - Split `Haskell` code in `.md`.-- **Special comments**:-  - Become comments in `.md`.-  - Can be supplied in a config via `lima --config <your config path>` ([sample config](./testdata/config/))-  - Should be written on a single line: `{- <COMMENTS> -}`-  - `{- LIMA_INDENT N -}` increases the indentation of snippets by `N`-  - `{- LIMA_DEDENT -}` sets the indentation of snippets to `0`-  - `{- LIMA_DISABLE -}` starts copying the following lines verbatim-  - `{- LIMA_ENABLE -}` stops copying the following lines verbatim-- **Code and single-line comments**:-  - Become `hs` snippets in `.md`+- [LiterateMarkdown](https://github.com/haskie-lambda/LiterateMarkdown) - `lima` is a fork of this (abandoned?) project. -### .lhs -> .md+- [pandoc](https://www.uv.es/wikibase/doc/cas/pandoc_manual_instalado.wiki?60) - [supports](https://www.uv.es/wikibase/doc/cas/pandoc_manual_instalado.wiki?60) `Literate Haskell` and a ton of other formats. -#### Examples+- [IHaskell](https://github.com/IHaskell/IHaskell) - create `Jupyter` notebooks with `Haskell` code cells and `GitHub Flavored Markdown` text cells and do much more. -1. [hs](./testdata/lhs/input0.lhs) -> [hs.md](./testdata/lhs/input0.lhs.md) -> [hs.md.hs](./testdata/lhs/input0.lhs.md.lhs)-1. [hs](./testdata/lhs/input1.lhs) -> [hs.md](./testdata/lhs/input1.lhs.md) -> [hs.md.hs](./testdata/lhs/input1.lhs.md.lhs)+- [lhs2tex](https://hackage.haskell.org/package/lhs2tex) - convert `Literate Haskell` to `TeX`. -#### Rules+## Supported formats -- **Text**:-  - As `.lhs` doesn't support `#` (heading) or `>` (quotation start) at a line beginning, write ` #` and ` >` instead. `lhs` -> `lhs.md` -> `lhs.md.lhs`:+- `Haskell` (`.hs`)+- `Literate Haskell` (`.lhs`)+- `GitHub Flavored Markdown` (`.md`)+- `TeX` (`.tex`) -    - <code>&nbsp;#</code> -> `#` -> <code>&nbsp;#</code>-    - <code>&nbsp;></code> -> `>` -> <code>&nbsp;></code>-- **Snippets**:+## Demo -  - `>` 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`-  - Snippets in <code>\`\`\`</code> become <code>\`\`\`console</code> and then `<`.-    - The round-trip property is not guarranteed+### Markdown -## Command-line tool+[.hs](./testdata/md/test.hs) and [.md](./testdata/md/test.md) -### From Hackage+![demo](https://raw.githubusercontent.com/deemp/lima/main/README/md-demo-hs-md-preview.png) -1. Install via cabal+### TeX -    ```console-    cabal update-    cabal install lima-    ```+[.hs](./testdata/tex/test.hs) and [.lhs](./testdata/tex/test.lhs) and [.tex](./testdata/tex/test.tex) -### From sources+![demo](https://raw.githubusercontent.com/deemp/lima/main/README/tex-demo-hs-lhs-tex.png) -1. Clone this repo and install `lima`.+## Ideas -    ```console-    git clone https://github.com/deemp/flakes-    cd flakes/lima-    cabal update-    cabal install .-    ```+- I introduced tags into supported formats.+  - E.g., in `.hs` files, tags are multiline comments written on a single line like `{- LIMA_ENABLE -}`.+- Tag names are configurable.+  - A user may set `on` instead of `LIMA_ENABLE`.+- A document can be parsed into a list of tokens.+  - Tags affect how a document is parsed.+- The tokens can be printed back to that document.+- After printing a parsed document back to itself, the document becomes formatted.+  - Formatting is idempotent. In other words, formatting the document again won't change its contents.+- The `lima` library provides a parser and a printer for each supported format.+- A composition of a printer after a parser produces a converter.+- Such a converter is usually invertible for two formatted documents.+  - `TeX` format requires special tags surrounding the `Haskell` code blocks for this property to hold. -### Nix+## Setup -1. [Install Nix](https://github.com/deemp/flakes/blob/main/README/InstallNix.md)+1. Create a test suite.+1. Add `lima` and `text` to its dependencies.+1. Create a test module. It can have the following contents. -1. Get `lima` on `PATH`.+    <!-- LIMA_INDENT 4 --> -    ```console-    nix flake lock github:deemp/flakes?dir=lima-    nix shell github:deemp/flakes?dir=lima-    lima --help+    ```haskell+    import Converter (Format (..), convertTo, def)+    import Data.Text.IO qualified as T+    +    main :: IO ()+    main = T.readFile "README.hs" >>= T.writeFile "README.md" . (Hs `convertTo` Md) def     ``` -### Windows--Warning: took it from [LiterateMarkdown](https://github.com/haskie-lambda/LiterateMarkdown).--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 executable to `C:/Users/username/AppData/Roaming/cabal/bin` and ensure that this directory is in your `PATH`.--## build-tool-depends--You can use `lima` to generate your docs, e.g., via `cabal test docs`. Just provide such a test with a script that converts (and, possibly, combines) files. As you'll use `lima` in a script, you should add it to that test's [build-tool-depends](https://cabal.readthedocs.io/en/3.8/cabal-package.html#pkg-field-build-tool-depends):+<!-- LIMA_DEDENT --> -  ```cabal-  build-tool-depends:-      lima:lima ==0.1.*-  ```+### Example -### Nix flake+This package has two such test suites: -1. Add `lima` to inputs:-  -  ```nix-  inputs.lima.url = "github:deemp/flakes?dir=lima";-  ```+- [readme-hs-to-md](test/HsToMd/Main.hs) converts `README.hs` to `README.md`+- [readme-md-to-hs](test/MdToHs/Main.hs) converts `README.md` to `README.hs` -1. Add `lima` to the override of your package+## Workflow -  ```nix-  override = {-    overrides = self: super: {-      myPackage = overrideCabal-        (super.callCabal2nix myPackageName ./. { })-        (x: {-          testHaskellDepends = [-            (super.callCabal2nix "lima" "${lima.outPath}/lima" { })-          ] ++ (x.testHaskellDepends or [ ]);-        });-    };-  };-  ```+Here's a possible workflow for `Haskell` and `Markdown`: -1. Use `cabal v1-test` so that `cabal` uses the supplied `lima`.+1. Edit the code in a `README.hs` using [Haskell Language Server](https://github.com/haskell/haskell-language-server).+1. Convert `README.hs` to a `README.md`. Comments from `README.hs` become text in `README.md`.+1. Edit the text in `README.md` using [markdownlint](https://github.com/DavidAnson/markdownlint).+1. Convert `README.md` back to the `README.hs` to keep files in sync. Text in `README.md` becomes comments in `README.hs`.+1. Repeat.  ## Contribute  Clone this repo and enter `lima`  ```console-git clone https://github.com/deemp/flakes-cd flakes/lima+git clone https://github.com/deemp/lima+cd lima ```  ### cabal -Build as usually+Build  ```console cabal update@@ -163,7 +106,7 @@  1. [Install](https://github.com/deemp/flakes/blob/main/README/InstallNix.md) `Nix`. -1. Run a devshell and build `lima`:+1. Run a devshell and build `lima` using the project's `cabal`:      ```console     nix develop nix-dev/
− app/Main.hs
@@ -1,143 +0,0 @@-{-# 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" <-> lparen <> bold "Haskell" <-> lparen <> bold ".hs" <> rparen-    , "or" <-> bold "Literate Haskell" <> lparen <> bold ".lhs" <> rparen <> rparen-    , "and" <-> bold "Markdown" <-> lparen <> bold ".md" <> rparen-    , 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 "MD" <+> "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 "MD" <> 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), "hs")-          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
@@ -1,15 +1,15 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.35.1.+-- This file has been generated from package.yaml by hpack version 0.35.2. -- -- see: https://github.com/sol/hpack  name:           lima-version:        0.1.0.6-synopsis:       (Haskell or Literate Haskell) <-> Markdown converter-description:    See the [README.md](https://github.com/deemp/flakes/tree/main/lima#readme)+version:        0.2.0.0+synopsis:       Convert between Haskell, Markdown, Literate Haskell, TeX+description:    See the [README.md](https://github.com/deemp/lima#readme) category:       Productivity-bug-reports:    https://github.com/deemp/flakes/issues+bug-reports:    https://github.com/deemp/lima/issues author:         Fabian Schneider maintainer:     Danila Danko license:        MIT@@ -18,20 +18,20 @@ extra-source-files:     CHANGELOG.md     README.md-    testdata/config/lima.yaml-    testdata/hs/input0.hs-    testdata/hs/input0.hs.md-    testdata/hs/input0.hs.md.hs-    testdata/lhs/input0.lhs-    testdata/lhs/input0.lhs.md-    testdata/lhs/input0.lhs.md.lhs-    testdata/lhs/input1.lhs-    testdata/lhs/input1.lhs.md-    testdata/lhs/input1.lhs.md.lhs+    testdata/md/test.hs+    testdata/md/test.lhs+    testdata/md/test.md+    testdata/md/test.tex+    testdata/md/tokens.hs+    testdata/tex/test.hs+    testdata/tex/test.lhs+    testdata/tex/test.md+    testdata/tex/test.tex+    testdata/tex/tokens.hs  source-repository head   type: git-  location: https://github.com/deemp/flakes+  location: https://github.com/deemp/lima  library   exposed-modules:@@ -40,40 +40,260 @@       Paths_lima   hs-source-dirs:       src+  default-extensions:+      BlockArguments+      ConstraintKinds+      DataKinds+      DeriveAnyClass+      DeriveDataTypeable+      DeriveGeneric+      DuplicateRecordFields+      ExistentialQuantification+      FlexibleInstances+      GADTs+      ImplicitParams+      ImportQualifiedPost+      InstanceSigs+      LambdaCase+      MultiWayIf+      NamedFieldPuns+      OverloadedStrings+      QuasiQuotes+      RankNTypes+      RecordWildCards+      ScopedTypeVariables+      StandaloneDeriving+      TemplateHaskell+      TupleSections+      TypeApplications+      TypeFamilies+      ViewPatterns+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints   build-depends:-      aeson-    , base ==4.*+      base ==4.*     , data-default-    , optparse-applicative-    , yaml+    , microlens+    , microlens-th+    , text   default-language: Haskell2010 -executable lima-  main-is: app/Main.hs+executable readme+  main-is: README.hs   other-modules:       Paths_lima+  default-extensions:+      BlockArguments+      ConstraintKinds+      DataKinds+      DeriveAnyClass+      DeriveDataTypeable+      DeriveGeneric+      DuplicateRecordFields+      ExistentialQuantification+      FlexibleInstances+      GADTs+      ImplicitParams+      ImportQualifiedPost+      InstanceSigs+      LambdaCase+      MultiWayIf+      NamedFieldPuns+      OverloadedStrings+      QuasiQuotes+      RankNTypes+      RecordWildCards+      ScopedTypeVariables+      StandaloneDeriving+      TemplateHaskell+      TupleSections+      TypeApplications+      TypeFamilies+      ViewPatterns+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints   build-depends:-      aeson-    , base ==4.*-    , data-default+      base ==4.*     , lima-    , optparse-applicative-    , yaml+    , microlens+    , text   default-language: Haskell2010 -test-suite lima-test+test-suite conversions   type: exitcode-stdio-1.0   main-is: Main.hs   other-modules:       Paths_lima   hs-source-dirs:-      test+      test/Conversions+  default-extensions:+      BlockArguments+      ConstraintKinds+      DataKinds+      DeriveAnyClass+      DeriveDataTypeable+      DeriveGeneric+      DuplicateRecordFields+      ExistentialQuantification+      FlexibleInstances+      GADTs+      ImplicitParams+      ImportQualifiedPost+      InstanceSigs+      LambdaCase+      MultiWayIf+      NamedFieldPuns+      OverloadedStrings+      QuasiQuotes+      RankNTypes+      RecordWildCards+      ScopedTypeVariables+      StandaloneDeriving+      TemplateHaskell+      TupleSections+      TypeApplications+      TypeFamilies+      ViewPatterns+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints   build-depends:-      aeson-    , base ==4.*-    , data-default+      base ==4.*+    , breakpoint+    , directory+    , hedgehog     , lima-    , optparse-applicative+    , microlens+    , pretty-simple     , string-interpolate-    , yaml+    , tasty+    , tasty-hedgehog+    , tasty-hunit+    , text+  default-language: Haskell2010++test-suite doctests+  type: exitcode-stdio-1.0+  main-is: Main.hs+  other-modules:+      Paths_lima+  hs-source-dirs:+      test/Doctests+  default-extensions:+      BlockArguments+      ConstraintKinds+      DataKinds+      DeriveAnyClass+      DeriveDataTypeable+      DeriveGeneric+      DuplicateRecordFields+      ExistentialQuantification+      FlexibleInstances+      GADTs+      ImplicitParams+      ImportQualifiedPost+      InstanceSigs+      LambdaCase+      MultiWayIf+      NamedFieldPuns+      OverloadedStrings+      QuasiQuotes+      RankNTypes+      RecordWildCards+      ScopedTypeVariables+      StandaloneDeriving+      TemplateHaskell+      TupleSections+      TypeApplications+      TypeFamilies+      ViewPatterns+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+  build-depends:+      base ==4.*+    , doctest-parallel+    , lima+    , microlens+    , text+  default-language: Haskell2010++test-suite readme-hs-to-md+  type: exitcode-stdio-1.0+  main-is: Main.hs+  other-modules:+      Paths_lima+  hs-source-dirs:+      test/HsToMd+  default-extensions:+      BlockArguments+      ConstraintKinds+      DataKinds+      DeriveAnyClass+      DeriveDataTypeable+      DeriveGeneric+      DuplicateRecordFields+      ExistentialQuantification+      FlexibleInstances+      GADTs+      ImplicitParams+      ImportQualifiedPost+      InstanceSigs+      LambdaCase+      MultiWayIf+      NamedFieldPuns+      OverloadedStrings+      QuasiQuotes+      RankNTypes+      RecordWildCards+      ScopedTypeVariables+      StandaloneDeriving+      TemplateHaskell+      TupleSections+      TypeApplications+      TypeFamilies+      ViewPatterns+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+  build-depends:+      base ==4.*+    , lima+    , microlens+    , text+  default-language: Haskell2010++test-suite readme-md-to-hs+  type: exitcode-stdio-1.0+  main-is: Main.hs+  other-modules:+      Paths_lima+  hs-source-dirs:+      test/MdToHs+  default-extensions:+      BlockArguments+      ConstraintKinds+      DataKinds+      DeriveAnyClass+      DeriveDataTypeable+      DeriveGeneric+      DuplicateRecordFields+      ExistentialQuantification+      FlexibleInstances+      GADTs+      ImplicitParams+      ImportQualifiedPost+      InstanceSigs+      LambdaCase+      MultiWayIf+      NamedFieldPuns+      OverloadedStrings+      QuasiQuotes+      RankNTypes+      RecordWildCards+      ScopedTypeVariables+      StandaloneDeriving+      TemplateHaskell+      TupleSections+      TypeApplications+      TypeFamilies+      ViewPatterns+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+  build-depends:+      base ==4.*+    , lima+    , microlens+    , text   default-language: Haskell2010
src/Converter.hs view
@@ -1,343 +1,1540 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE MultiWayIf #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# HLINT ignore "Redundant bracket" #-}-{-# LANGUAGE TypeApplications #-}-{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}---- | 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.Foldable (Foldable (..))-import Data.Function ((&))-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_ :: Int -> String-backticks_ n = indentN n backticks--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"--_LIMA_INDENT :: String-_LIMA_INDENT = "LIMA_INDENT"--_LIMA_DEDENT :: String-_LIMA_DEDENT = "LIMA_DEDENT"---- | 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", _LIMA_INDENT, _LIMA_DEDENT]--endsWith :: String -> String -> Bool-endsWith = flip isSuffixOf---- multi-line comments-mcOpen :: String-mcOpen = "{-"--mcOpenSpace :: String-mcOpenSpace = mcOpen ++ " "---- | multiline comment closing symbols-mcClose :: String-mcClose = "-}"--mcCloseSpace :: String-mcCloseSpace = " -}"--dropEnd :: Int -> String -> String-dropEnd n s = reverse (drop n (reverse s))--backticksHs :: String-backticksHs = backticks ++ "haskell"--backticksHs_ :: Int -> String-backticksHs_ n = indentN n backticksHs--dropEmpties :: [String] -> [String]-dropEmpties = dropWhile (\x -> dropSpaces x == "")---- Markdown comments---- | Open a @Markdown@ comment-mdcOpen :: String-mdcOpen = "<!--"---- | Close a @Markdown@ comment-mdcClose :: String-mdcClose = "-->"--mdcOpenSpace :: String-mdcOpenSpace = mdcOpen ++ " "--mdcCloseSpace :: String-mdcCloseSpace = " " ++ mdcClose---- | Remove multiline comment marks from the beginning and end of a string-stripMC :: String -> String-stripMC x = dropEnd (length mcCloseSpace) (drop (length mcOpenSpace) x)--indentN :: Int -> String -> String-indentN x s = concat (replicate x " ") ++ s---- | 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 0 x []) . lines- where-  specialComments_ = specialCommentsDefault ++ specialComments-  convert :: Bool -> Bool -> Bool -> Int -> [String] -> [String] -> [String]-  convert inLimaEnable inComments inSnippet indent_ (h : hs) res-    | not inComments =-        if-            | -- disable-              -- split a snippet-              h == mcOpenSpace ++ _LIMA_DISABLE ++ mcCloseSpace ->-                (convert False False False indent_ hs)-                  ([mdcOpenSpace ++ _LIMA_DISABLE] ++ [backticks_ indent_ | inSnippet] ++ dropEmpties res)-            | -- enable-              h == mcOpenSpace ++ _LIMA_ENABLE ++ mcCloseSpace ->-                convert True False inSnippet indent_ hs ((_LIMA_ENABLE ++ mdcCloseSpace) : res)-            | -- disabled-              not inLimaEnable ->-                convert inLimaEnable False False indent_ hs (h : res)-            | -- indentation comment-              -- splits a snippet-              h `startsWithAnyOf` ((mcOpenSpace ++) <$> [_LIMA_INDENT, _LIMA_DEDENT]) ->-                let h_ = stripMC h-                    isIndent = h_ `startsWith` _LIMA_INDENT-                    newIndent_ = if isIndent then indent_ + (read @Int $ drop (length _LIMA_INDENT) h_) else 0-                    curIndent_ = if isIndent then newIndent_ else indent_-                 in (convert inLimaEnable False False newIndent_ hs)-                      ( [indentN curIndent_ $ mdcOpenSpace ++ stripMC h ++ mdcCloseSpace]-                          ++ ["" | inSnippet]-                          ++ [backticks_ indent_ | inSnippet]-                          ++ (if isIndent then squashEmpties else dropEmpties) res-                      )-            | -- a special comment-              -- a comment should be in multi-line style like '{- FOURMOLU_DISABLE -}'-              -- it should occupy a single line-              -- it may be followed by arbitrary text in that line-              -- splits a snippet-              h `startsWithAnyOf` ((mcOpenSpace ++) <$> specialComments_) ->-                (convert inLimaEnable False False indent_ hs)-                  ([mdcOpenSpace ++ stripMC h ++ mdcCloseSpace] ++ ["" | inSnippet] ++ [backticks_ indent_ | 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_ indent_ | 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 indent_ hs ([dropEnd (length mcCloseSpace) x'] ++ pref ++ res')-                      else convert inLimaEnable True False indent_ 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 indent_ hs ([indentN indent_ h, backticksHs_ indent_] ++ squashEmpties res)-                  else -- if not in snippet, collapse consequent empty lines-                    convert inLimaEnable False False indent_ hs res-            | inSnippet ->-                convert inLimaEnable False True indent_ hs ((indentN indent_ h) : res)-    | inComments =-        if h `startsWith` mcClose-          then -- end of a multiline comment-            convert inLimaEnable False False indent_ hs (res)-          else -- copy everything from comments-            convert inLimaEnable True False indent_ hs (h : res)-  convert _ _ inSnippet indent_ [] res =-    [backticks_ indent_ | inSnippet] ++ dropEmpties res--stripMdc :: String -> String-stripMdc x = dropEnd (length mdcCloseSpace) (drop (length mdcOpenSpace) (dropSpaces x))--squashEmpties :: [String] -> [String]-squashEmpties = ([""] ++) . dropEmpties--dropSpaces :: String -> String-dropSpaces = dropWhile (== ' ')--countSpaces :: String -> Int-countSpaces x = length $ takeWhile (== ' ') x---- | 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 0 x []) . lines- where-  specialComments_ = specialCommentsDefault ++ specialComments-  closeTextIf x = [mcClose | x]-  convert :: Bool -> Bool -> Bool -> Int -> [String] -> [String] -> [String]-  convert inText inSnippet inComments dedent_ (h : hs) res-    | inComments =-        -- enable-        if h `startsWith` (_LIMA_ENABLE ++ mdcCloseSpace)-          then -- split text--            (convert inText inSnippet False dedent_ hs)-              ([mcOpenSpace ++ _LIMA_ENABLE ++ mcCloseSpace] ++ ["" | inText] ++ closeTextIf inText ++ res)-          else -- copy lines-            convert False False True dedent_ hs (h : res)-    | not inSnippet =-        if-            | -- found a disable comment-              h `startsWith` (mdcOpenSpace ++ _LIMA_DISABLE) ->-                (convert False False True dedent_ hs)-                  ([mcOpenSpace ++ _LIMA_DISABLE ++ mcCloseSpace, ""] ++ closeTextIf inText ++ res)-            | -- found a special comment-              (dropSpaces h) `startsWithAnyOf` ((mdcOpenSpace ++) <$> specialComments_) ->-                (convert False False False dedent_ hs)-                  ([mcOpenSpace ++ stripMdc h ++ mcCloseSpace] ++ [""] ++ closeTextIf inText ++ dropEmpties res)-            | -- start of a haskell snippet-              (dropSpaces h) `startsWith` backticksHs ->-                convert False True False (countSpaces h) hs ([""] ++ closeTextIf inText ++ dropEmpties res)-            | not inText ->-                if null h-                  then -- just a blank line-                    convert inText False False dedent_ hs res-                  else -- start of text-                    convert True False False dedent_ hs ([h, mcOpen, ""] ++ res)-            | -- copy text line by line-              otherwise ->-                convert True False False dedent_ hs (h : res)-    | otherwise =-        if (dropSpaces h) == backticks-          then -- end of a snippet-            convert False False False 0 hs res-          else -- in a snippet-            convert False True False dedent_ hs ((drop dedent_ h) : res)-  convert inText _ _ dedent_ [] res = [mcClose | inText] ++ res+-- |+-- == Terms #terms#+--+-- * @format@ - specific encoding of some information. See 'Format'.+-- * @document@ - 'T.Text' in a specific format, e.g., @Haskell@ (@.hs@) file.+-- * @document block@ - consecutive lines of a document.+-- * 'Token' - a representation of a document block as a @Haskell@ type.+-- * 'Tokens' - a list of 'Token's.+-- * @parser@ - a function that reads a document line by line and converts it to 'Token's. Example: 'hsToTokens'.+-- * @printer@ - a function that converts 'Tokens' to a document. Example: 'hsFromTokens'.+-- * @tag@ - a marker that affects how 'Tokens' are parsed.+--+--     * Each parser recognizes tags of a specific form.+--     * Tags can be represented as a wrapper and a name.+--+--         E.g., in @'% LIMA_DISABLE some text'@, a @TeX@ tag, the wrapper is @'% '@ and the name is @'LIMA_DISABLE some text'@.+--+--     * Parsers recognize the tag names that /start with/ tag names specified in a 'Config'.+--+--         E.g., in the example above, a parser will recognize the [_disable](#v:_disable) tag and will become disabled.+--+--     * When a parser is disabled, it copies lines verbatim into a 'Disabled' 'Token' and doesn't recognize any tags until it finds an [_enable](#v:_enable) tag.+--+-- == Assumptions #assumptions#+--+-- The following assumptions must hold for outputs of parsers and inputs of printers:+--+--     - 'Tokens' are in the same order as the corresponding blocks of document.+--     - Lines inside 'Tokens' are reversed compared to the document. Example:+--+--         - @Literate Haskell@ document:+--+--             @+--             line 1+--             line 2+--+--             % line 3+--+--             % line 4+--             @+--+--         - Corresponding 'Tokens':+--+--             @+--             [+--               Text {manyLines = ["line2","line 1"]},+--               Comment {someLines = "line 4" :| ["", "line 3"]}+--             ]+--             @+--+--      - There are no leading or trailing empty lines inside of 'Tokens'.+module Converter (+  -- * Config+  Mode,+  User,+  Internal,+  Config (..),+  def,+  toInternalConfig,+  fromInternalConfig,++  -- ** Lenses+  disable,+  enable,+  indent,+  dedent,+  mdHaskellCodeStart,+  mdHaskellCodeEnd,+  texHaskellCodeStart,+  texHaskellCodeEnd,++  -- * microlens+  (&),+  (?~),++  -- * Format+  Format (..),+  convertTo,+  showFormatExtension,+  showFormatName,++  -- * Tokens+  Token (..),+  Tokens,+  selectFromTokens,+  selectToTokens,+  mergeTokens,+  stripTokens,+  normalizeTokens,++  -- * Printers+  hsFromTokens,+  hsFromTokens',+  lhsFromTokens,+  lhsFromTokens',+  mdFromTokens,+  mdFromTokens',+  texFromTokens,+  texFromTokens',++  -- * Parsers+  lhsToTokens,+  hsToTokens,+  texToTokens,+  mdToTokens,++  -- * Examples+  exampleNonTexTokens',+  exampleNonTexTokens,+  exampleTexTokens,++  -- * Helpers+  stripEmpties,+  PrettyPrint (..),+) where++import Data.Char (isAlpha)+import Data.Data (Data (toConstr), showConstr)+import Data.Default (Default (def))+import Data.List (dropWhileEnd, intersperse)+import Data.List.NonEmpty (NonEmpty ((:|)), fromList, toList, (<|))+import Data.Text qualified as T+import GHC.Generics (Generic)+import Lens.Micro (non, (&), (?~), (^.), (^?))+import Lens.Micro.TH (makeLenses)+import Text.Read (readMaybe)+import Text.Show qualified as T++-- | A kind of data markers.+data Mode'+  = Internal+  | User++-- | Marks data for internal usage.+type Internal = 'Internal++-- | Marks data supplied by a user.+type User = 'User++-- | Calculates the mode for data.+type family Mode a b where+  Mode User b = Maybe b+  Mode Internal b = b++-- | Configuration of tag names.+--+-- Here are the default names.+--+-- >>> pp (def :: Config User)+-- Config {+--   _disable = Just "LIMA_DISABLE",+--   _enable = Just "LIMA_ENABLE",+--   _indent = Just "LIMA_INDENT",+--   _dedent = Just "LIMA_DEDENT",+--   _mdHaskellCodeStart = Just "```haskell",+--   _mdHaskellCodeEnd = Just "```",+--   _texHaskellCodeStart = Just "\\begin{code}",+--   _texHaskellCodeEnd = Just "\\end{code}"+-- }+--+-- It's possible to override these names.+--+-- >>> pp ((def :: Config User) & disable ?~ "off" & enable ?~ "on" & indent ?~ "indent" & dedent ?~ "dedent")+-- Config {+--   _disable = Just "off",+--   _enable = Just "on",+--   _indent = Just "indent",+--   _dedent = Just "dedent",+--   _mdHaskellCodeStart = Just "```haskell",+--   _mdHaskellCodeEnd = Just "```",+--   _texHaskellCodeStart = Just "\\begin{code}",+--   _texHaskellCodeEnd = Just "\\end{code}"+-- }+data Config (a :: Mode') = Config+  { _disable :: Mode a T.Text+  -- ^+  -- Make parser ignore tags and just copy the following lines verbatim.+  --+  -- Set indentation to @0@.+  , _enable :: Mode a T.Text+  -- ^ Stop parser from ignoring tags.+  , _indent :: Mode a T.Text+  -- ^ Set code indentation to a given 'Int'.+  , _dedent :: Mode a T.Text+  -- ^ Set code indentation to @0@.+  , _mdHaskellCodeStart :: Mode a T.Text+  -- ^ Mark the start of a @Haskell@ code block in @Markdown@.+  , _mdHaskellCodeEnd :: Mode a T.Text+  -- ^ Mark the end of a @Haskell@ code block in @Markdown@.+  , _texHaskellCodeStart :: Mode a T.Text+  -- ^ Mark the start of a @Haskell@ code block in @TeX@.+  , _texHaskellCodeEnd :: Mode a T.Text+  -- ^ Mark the end of a @Haskell@ code block in @TeX@.+  }+  deriving (Generic)++makeLenses ''Config++deriving instance Show (Config User)+deriving instance Eq (Config User)+deriving instance Show (Config Internal)++newtype Pretty a = Pretty String++instance Show a => Show (Pretty a) where+  show :: Pretty a -> String+  show (Pretty s) = s++-- >>> lhsFromTokens def ex+-- "line 1\nline2\n\n% line 3\n\n% line 4\n"++-- | A class for prettyprinting data on multiple lines in haddocks.+--+-- It's not meant to be used outside of this library.+class Show a => PrettyPrint a where+  pp :: a -> Pretty String++instance PrettyPrint String where+  pp :: String -> Pretty String+  pp = Pretty . dropWhileEnd (== '\n')++instance PrettyPrint T.Text where+  pp :: T.Text -> Pretty String+  pp = pp . T.unpack++instance PrettyPrint (Config User) where+  pp :: Config User -> Pretty String+  pp (fromInternalConfig . toInternalConfig -> config) =+    pp $+      ( concatMap+          ( \(a, b) ->+              if+                  | [a, b] == " _" -> "\n  "+                  | [a, b] == "{_" -> "{\n  "+                  | otherwise -> [a]+          )+          $ (zip (show config) (tail $ show config))+      )+        <> "\n}"++instance Default (Config Internal) where+  def :: Config Internal+  def = Config{..}+   where+    _disable = "LIMA_DISABLE"+    _enable = "LIMA_ENABLE"+    _indent = "LIMA_INDENT"+    _dedent = "LIMA_DEDENT"+    _mdHaskellCodeStart = "```haskell"+    _mdHaskellCodeEnd = "```"+    _texHaskellCodeStart = "\\begin{code}"+    _texHaskellCodeEnd = "\\end{code}"++-- | Make a user 'Config' with default values from an internal 'Config'.+fromInternalConfig :: Config Internal -> Config User+fromInternalConfig conf = Config{..}+ where+  _disable = conf ^? disable+  _enable = conf ^? enable+  _indent = conf ^? indent+  _dedent = conf ^? dedent+  _mdHaskellCodeStart = conf ^? mdHaskellCodeStart+  _mdHaskellCodeEnd = conf ^? mdHaskellCodeEnd+  _texHaskellCodeStart = conf ^? texHaskellCodeStart+  _texHaskellCodeEnd = conf ^? texHaskellCodeEnd++instance Default (Config User) where+  def :: Config User+  def = fromInternalConfig def++-- | Convert a user 'Config' to an internal 'Config' with user-supplied values.+toInternalConfig :: Config User -> Config Internal+toInternalConfig conf =+  Config+    { _disable = conf ^. disable . non _disable+    , _enable = conf ^. enable . non _enable+    , _indent = conf ^. indent . non _indent+    , _dedent = conf ^. dedent . non _dedent+    , _mdHaskellCodeStart = conf ^. mdHaskellCodeStart . non _mdHaskellCodeStart+    , _mdHaskellCodeEnd = conf ^. mdHaskellCodeEnd . non _mdHaskellCodeEnd+    , _texHaskellCodeStart = conf ^. texHaskellCodeStart . non _texHaskellCodeStart+    , _texHaskellCodeEnd = conf ^. texHaskellCodeEnd . non _texHaskellCodeEnd+    }+ where+  Config{..} = def @(Config Internal)++-- | A format of a document.+data Format+  = -- | @Haskell@+    Hs+  | -- | @Literate Haskell@+    Lhs+  | -- | @Markdown@+    Md+  | -- | @TeX@+    TeX++-- | Internal representation of a document.+--+-- A printer processes a list of 'Token's one by one.+--+-- A 'Token' can have:+--+-- - Action - how this 'Token' affects the subsequent 'Tokens'.+-- - Target - a type of 'Token's that are affected by this 'Token'.+-- - Range - the nearest 'Token' until which this 'Token' affects the subsequent 'Token's.+data Token+  = -- |+    -- - Action: set indentation to @n@.+    --+    -- - Target: 'HaskellCode'.+    --+    -- - Range: until 'Indent', 'Dedent', or 'Disabled'.+    Indent {n :: Int}+  | -- |+    -- - Action: set indentation to @0@.+    --+    -- - Target: 'HaskellCode'.+    --+    -- - Range: until 'Indent', 'Dedent', or 'Disabled'.+    Dedent+  | -- | A block that should be invisible when rendered outside of @.hs@.+    --+    -- - Action: set indentation to @0@.+    --+    -- - Target: 'HaskellCode'.+    --+    -- - Range: until 'Indent', 'Dedent', or 'Disabled'.+    Disabled {manyLines :: [T.Text]}+  | -- | Lines copied verbatim while a parser was in a @Haskell@ code block.+    HaskellCode {manyLines :: [T.Text]}+  | -- | Lines copied verbatim while a parser was in a text block.+    Text {someLines :: NonEmpty T.Text}+  | -- | Lines copied verbatim while a parser was in a comment block.+    Comment {someLines :: NonEmpty T.Text}+  deriving (Show, Data, Eq)++-- | A list of 'Token's.+type Tokens = [Token]++instance PrettyPrint (Tokens) where+  pp :: Tokens -> Pretty String+  pp ts =+    Pretty $+      ( concatMap+          ( \(a, b) ->+              if+                  | a == ',' && isAlpha b -> ",\n  "+                  | a == '[' && isAlpha b -> "[\n  "+                  | otherwise -> [a]+          )+          $ (zip (show ts) (tail $ show ts))+      )+        <> "\n]"++-- | Merge specific consecutive 'Tokens'.+--+-- >>> pp exampleNonTexTokens'+-- [+--   Indent {n = 3},+--   Disabled {manyLines = ["-- What's the answer?"]},+--   Indent {n = 1},+--   Indent {n = 2},+--   Text {someLines = "- Intermediate results" :| []},+--   HaskellCode {manyLines = ["   b = a 4","   a = const 3"]},+--   Dedent,+--   HaskellCode {manyLines = ["answer = b * 14"]},+--   Comment {someLines = "Hello from comments," :| []},+--   Comment {someLines = "world!" :| []},+--   Text {someLines = "world!" :| ["Hello from text,"]},+--   Text {someLines = "here!" :| ["And from"]}+-- ]+--+-- >>> pp $ mergeTokens exampleNonTexTokens'+-- [+--   Indent {n = 3},+--   Disabled {manyLines = ["-- What's the answer?"]},+--   Indent {n = 1},+--   Indent {n = 2},+--   Text {someLines = "- Intermediate results" :| []},+--   HaskellCode {manyLines = ["   b = a 4","   a = const 3"]},+--   Dedent,+--   HaskellCode {manyLines = ["answer = b * 14"]},+--   Comment {someLines = "world!" :| ["","Hello from comments,"]},+--   Text {someLines = "here!" :| ["And from","","world!","Hello from text,"]}+-- ]+mergeTokens :: Tokens -> Tokens+mergeTokens (t1@Text{} : t2@Text{} : ts) = mergeTokens $ Text{someLines = someLines t2 <> (T.empty <| someLines t1)} : ts+mergeTokens (t1@Comment{} : t2@Comment{} : ts) = mergeTokens $ Comment{someLines = someLines t2 <> (T.empty <| someLines t1)} : ts+mergeTokens (t : ts) = t : mergeTokens ts+mergeTokens ts = ts++-- | Example non-@TeX@ 'Tokens'. See 'exampleTexTokens'.+--+-- When printed to a @TeX@ document, these 'Tokens' can't be correctly parsed.+-- This is because they don't have necessary tags surrounding @Haskell@ code blocks.+--+-- >>> pp $ exampleNonTexTokens'+-- [+--   Indent {n = 3},+--   Disabled {manyLines = ["-- What's the answer?"]},+--   Indent {n = 1},+--   Indent {n = 2},+--   Text {someLines = "- Intermediate results" :| []},+--   HaskellCode {manyLines = ["   b = a 4","   a = const 3"]},+--   Dedent,+--   HaskellCode {manyLines = ["answer = b * 14"]},+--   Comment {someLines = "Hello from comments," :| []},+--   Comment {someLines = "world!" :| []},+--   Text {someLines = "world!" :| ["Hello from text,"]},+--   Text {someLines = "here!" :| ["And from"]}+-- ]+exampleNonTexTokens' :: Tokens+exampleNonTexTokens' =+  [ Indent 3+  , Disabled{manyLines = ["-- What's the answer?"]}+  , Indent 1+  , Indent 2+  , Text ("- Intermediate results" :| [])+  , HaskellCode ["   b = a 4", "   a = const 3"]+  , Dedent+  , HaskellCode ["answer = b * 14"]+  , Comment ("Hello from comments," :| [])+  , Comment ("world!" :| [])+  , Text ("world!" :| ["Hello from text,"])+  , Text ("here!" :| ["And from"])+  ]++-- | Normalized 'exampleNonTexTokens''.+--+-- >>>pp $ exampleNonTexTokens+-- [+--   Indent {n = 3},+--   Disabled {manyLines = ["-- What's the answer?"]},+--   Indent {n = 1},+--   Indent {n = 2},+--   Text {someLines = "- Intermediate results" :| []},+--   HaskellCode {manyLines = ["b = a 4","a = const 3"]},+--   Dedent,+--   HaskellCode {manyLines = ["answer = b * 14"]},+--   Comment {someLines = "world!" :| ["","Hello from comments,"]},+--   Text {someLines = "here!" :| ["And from","","world!","Hello from text,"]}+-- ]+exampleNonTexTokens :: Tokens+exampleNonTexTokens = normalizeTokens exampleNonTexTokens'++-- | Select a printer function based on a given format.+selectFromTokens :: Config User -> Format -> Tokens -> T.Text+selectFromTokens config format =+  ( case format of+      Hs -> hsFromTokens+      Lhs -> lhsFromTokens+      Md -> mdFromTokens+      TeX -> texFromTokens+  )+    config++-- | Select a parser function based on a given format.+selectToTokens :: Config User -> Format -> T.Text -> Tokens+selectToTokens config format =+  ( case format of+      Hs -> hsToTokens+      Lhs -> lhsToTokens+      Md -> mdToTokens+      TeX -> texToTokens+  )+    config++-- | Compose a function that converts a document in one 'Format' to a document in another 'Format'.+convertTo :: Format -> Format -> Config User -> T.Text -> T.Text+convertTo a b config src = selectFromTokens config b $ selectToTokens config a src++-- | Escaped hash character+escapedHash :: T.Text+escapedHash = "\\#"++-- | Hash character+hash :: T.Text+hash = "#"++-- | Drop a prefix of a line with length of a given line+dropLen :: T.Text -> T.Text -> T.Text+dropLen x y = T.drop (T.length x) y++-- | Check if a list starts with a given list+startsWith :: T.Text -> T.Text -> Bool+startsWith = flip T.isPrefixOf++-- | Check if a list ends with a given list+endsWith :: T.Text -> T.Text -> Bool+endsWith = flip T.isSuffixOf++-- | Drop leading spaces and drop at each end of a 'T.Text' the number of characters as in the supplied prefix and suffix.+stripEnds :: T.Text -> T.Text -> T.Text -> T.Text+stripEnds prefix suffix x = T.dropEnd (T.length suffix) (dropLen prefix (stripSpaces x))++-- | Replace "\\#" with "#" in a 'T.Text' prefix.+lhsUnescapeHash :: T.Text -> T.Text+lhsUnescapeHash x = if x `startsWith` escapedHash then hash <> (dropLen escapedHash x) else x++-- | Replace "#" with "\\#" in a 'T.Text' prefix.+lhsEscapeHash :: T.Text -> T.Text+lhsEscapeHash x = if x `startsWith` hash then escapedHash <> (dropLen hash x) else x++-- | State of a parser.+--+-- Only one flag can be enabled when processing a line.+--+-- Flags signify in what document block a converter is at the moment.+data State = State+  { inText :: Bool+  , inHaskellCode :: Bool+  , inDisabled :: Bool+  , inComment :: Bool+  }+  deriving (Generic)++instance Default State where+  def :: State+  def =+    State+      { inText = False+      , inHaskellCode = False+      , inDisabled = False+      , inComment = False+      }++-- | 'mergeTokens' and 'stripTokens'.+--+-- >>>pp $ normalizeTokens exampleNonTexTokens+-- [+--   Indent {n = 3},+--   Disabled {manyLines = ["-- What's the answer?"]},+--   Indent {n = 1},+--   Indent {n = 2},+--   Text {someLines = "- Intermediate results" :| []},+--   HaskellCode {manyLines = ["b = a 4","a = const 3"]},+--   Dedent,+--   HaskellCode {manyLines = ["answer = b * 14"]},+--   Comment {someLines = "world!" :| ["","Hello from comments,"]},+--   Text {someLines = "here!" :| ["And from","","world!","Hello from text,"]}+-- ]+normalizeTokens :: Tokens -> Tokens+normalizeTokens tokens = stripTokens $ mergeTokens $ tokens++-- | Compose a function from a 'T.Text' to 'Tokens'.+mkIntoTokens :: (State -> [(Int, T.Text)] -> [Token] -> [Token]) -> T.Text -> Tokens+mkIntoTokens toTokens xs = normalizeTokens (drop 1 $ reverse $ toTokens def (zip [1 ..] (T.lines xs)) [Dedent])++-- | Parse to a token contents of a multiline comment written on a single line.+--+-- Merge consecutive 'Comment's+parseToken :: Config Internal -> Token -> T.Text -> Int -> Tokens+parseToken Config{..} prev l lineNumber+  | l `startsWith` _indent =+      maybe+        (error $ "Expected a number at line: " <> show lineNumber)+        (\x -> [Indent (max 0 x), prev])+        (readMaybe @Int (T.unpack (dropLen _indent l)))+  | l == _dedent = [Dedent, prev]+  | otherwise =+      case prev of+        Comment{..} -> [Comment{someLines = l <| someLines}]+        _ -> [Comment (l :| []), prev]++-- | Show error with line number for a token.+errorExpectedToken :: (Data a1, Show a2, Show a3) => a2 -> a3 -> a1 -> a4+errorExpectedToken lineNumber lastToken expectedToken =+  error $+    ("Wrong state at line: " <> show lineNumber <> ".\n\n")+      <> ("Please, create an issue in the package repository.\n\n")+      <> ("Expected last token: " <> constructorName expectedToken <> "\n\n")+      <> ("Got last token: " <> show lastToken <> "\n\n")++-- | Strip the given value from the beginning and the end of a list.+stripList :: Eq a => a -> [a] -> [a]+stripList x = dropWhileEnd (== x) . dropWhile (== x)++-- | Pad a 'T.Text' with a given number of spaces+indentN :: Int -> T.Text -> T.Text+indentN x s = T.concat (replicate x " ") <> s++-- | Compose a function from 'Tokens' to a 'T.Text'.+mkFromTokens :: (Config User -> Tokens -> [T.Text]) -> Config User -> Tokens -> T.Text+mkFromTokens f' config = (<> "\n") . T.intercalate "\n" . f' config++-- | same as 'exampleNonTexTokens', but with @TeX@-specific tags that make @Haskell@ code blocks correctly parsable.+--+-- >>> pp $ exampleTexTokens+-- [+--   Disabled {manyLines = ["-- What's the answer?"]},+--   Indent {n = 1},+--   Indent {n = 2},+--   Text {someLines = "\\begin{code}" :| ["","Intermediate results"]},+--   HaskellCode {manyLines = ["b = a 4","a = const 3"]},+--   Text {someLines = "\\end{code}" :| []},+--   Dedent,+--   Text {someLines = "\\begin{code}" :| []},+--   HaskellCode {manyLines = ["answer = b * 14"]},+--   Text {someLines = "\\end{code}" :| []},+--   Comment {someLines = "world!" :| ["","Hello from comments,"]},+--   Text {someLines = "world!" :| ["Hello from text,"]}+-- ]+exampleTexTokens :: Tokens+exampleTexTokens =+  normalizeTokens $+    [ Disabled{manyLines = ["-- What's the answer?"]}+    , Indent 1+    , Indent 2+    , Text{someLines = "Intermediate results" :| []}+    , Text{someLines = "\\begin{code}" :| []}+    , HaskellCode ["   b = a 4", "   a = const 3"]+    , Text{someLines = "\\end{code}" :| []}+    , Dedent+    , Text{someLines = "\\begin{code}" :| []}+    , HaskellCode ["answer = b * 14"]+    , Text{someLines = "\\end{code}" :| []}+    , Comment ("Hello from comments," :| [])+    , Comment ("world!" :| [])+    , Text{someLines = "world!" :| ["Hello from text,"]}+    ]++-- | Convert 'Tokens' to @TeX@ code.+--+-- __Rules__+--+-- - Certain [assumptions]("Converter#assumptions") must hold for inputs.+-- - These are the relations between document blocks and tokens when the default 'Config' values are used.+--+--     - @'% LIMA_INDENT N'@ (@N@ is an 'Int') ~ 'Indent'+--     - @'% LIMA_DEDENT'@ ~ 'Dedent'.+--     - Lines between and including @'% LIMA_DISABLE'@ and @'% LIMA_ENABLE'@ ~ 'Disabled'.+--+--     - Consecutive lines, either empty or starting with @'% '@ ~ 'Comment'.+--+--         @+--         % Hello,+--         % world!+--+--         % Hello,+--         % user!+--         @+--+--         - At least one line must have nonempty text after @'% '@+--+--     - Lines between possibly indented tags @'\\begin{code}'@ and @'\\end{code}'@ ~ 'HaskellCode'.+--+--     - Other lines ~ 'Text'.+--+-- === __Example__+--+-- >>> pp $ texFromTokens def exampleTexTokens+-- % LIMA_DISABLE+-- <BLANKLINE>+-- % -- What's the answer?+-- <BLANKLINE>+-- % LIMA_ENABLE+-- <BLANKLINE>+-- % LIMA_INDENT 1+-- <BLANKLINE>+-- % LIMA_INDENT 2+-- <BLANKLINE>+-- Intermediate results+-- <BLANKLINE>+-- \begin{code}+--   a = const 3+--   b = a 4+-- \end{code}+-- <BLANKLINE>+-- % LIMA_DEDENT+-- <BLANKLINE>+-- \begin{code}+-- answer = b * 14+-- \end{code}+-- <BLANKLINE>+-- % Hello from comments,+-- <BLANKLINE>+-- % world!+-- <BLANKLINE>+-- Hello from text,+-- world!+texFromTokens :: Config User -> Tokens -> T.Text+texFromTokens = mkFromTokens texFromTokens'++-- | Start a @TeX@ comment.+texComment :: T.Text+texComment = "%"++-- | Start a @TeX@ comment plus a space.+texCommentSpace :: T.Text+texCommentSpace = texComment <> " "++-- | Convert 'Tokens' to @TeX@ code.+--+-- Each 'Token' becomes a 'T.Text' in a list.+--+-- These 'T.Text's are concatenated in 'texFromTokens'.+texFromTokens' :: Config User -> Tokens -> [T.Text]+texFromTokens' (toInternalConfig -> Config{..}) tokens =+  dropEmpties $ reverse $ (T.intercalate "\n" . reverse <$> (fromTokens (Dedent : tokens) (0, [])))+ where+  fromTokens :: Tokens -> (Int, [[T.Text]]) -> [[T.Text]]+  fromTokens bs'@(_ : cur : bs) (curIndent, rs) =+    fromTokens (cur : bs) (translate curIndent bs' rs)+  fromTokens [_] (_, rs) = rs+  fromTokens _ _ = errorNotEnoughTokens TeX+  translate curIndent (prev : cur : _) rs =+    case cur of+      Indent{..} -> (n,) $ [texCommentSpace <> _indent <> " " <> T.pack (show n)] : [] : rs+      Dedent -> (0,) $ [texCommentSpace <> _dedent] : [] : rs+      Disabled{..} -> (0,) $ [[texCommentSpace <> _enable], [], (prependTexComment <$> manyLines), [], [texCommentSpace <> _disable], []] <> rs+      HaskellCode{..} ->+        (curIndent,) $+          (indentN curIndent <$> manyLines)+            : ( case prev of+                  Text{} -> rs+                  _ -> [] : rs+              )+      Text{..} ->+        (curIndent,) $+          toList someLines+            : ( case prev of+                  HaskellCode{} -> rs+                  _ -> [] : rs+              )+      Comment{someLines = t :| ts} -> (curIndent, (prependTexComment <$> (t : ts)) : [] : rs)+  translate _ _ _ = errorNotEnoughTokens TeX++errorNotEnoughTokens :: Format -> a+errorNotEnoughTokens format = error $ "Got not enough tokens when converting 'Tokens' to " <> showFormatName format++-- | Drop spaces at the start and the end of a 'T.Text'.+stripSpaces :: T.Text -> T.Text+stripSpaces = T.strip++-- | Prepend start of a @TeX@ comment (@'% '@) to a 'T.Text'.+prependTexComment :: T.Text -> T.Text+prependTexComment l+  | l == T.empty = l+  | otherwise = texCommentSpace <> l++-- | Drop start of a @TeX@ comment from a 'T.Text'.+dropTexComment :: Show a => T.Text -> a -> T.Text+dropTexComment l lineNumber+  | l `startsWith` texCommentSpace = dropLen texCommentSpace l+  | l == T.empty = l+  | otherwise =+      error $+        "Lines in a 'Disabled' block must either be empty or start with '% '\n\n"+          <> "Note that each 'Disabled' block must have at least one line starting with '% ' and having nonempty text after '% ' "+          <> ("The line " <> show lineNumber <> " must either be empty or start with '% '")++-- | Convert 'Tokens' to @TeX@ code.+--+-- Inverse of 'texFromTokens'.+--+-- >>> (texToTokens def $ texFromTokens def exampleTexTokens) == exampleTexTokens+-- True+texToTokens :: Config User -> T.Text -> Tokens+texToTokens (toInternalConfig -> conf@Config{..}) xs = tokens+ where+  tokens = mkIntoTokens toTokens xs+  toTokens :: State -> [(Int, T.Text)] -> Tokens -> Tokens+  toTokens State{..} ((lineNumber, l) : ls) result@(r : rs)+    | inDisabled =+        if+            | -- enable+              l `startsWith` (texCommentSpace <> _enable) ->+                toTokens def ls result+            | -- copy lines+              otherwise ->+                toTokens def{inDisabled} ls $+                  case r of+                    Disabled{..} -> (r{manyLines = dropTexComment l lineNumber : manyLines} : rs)+                    _ -> errorExpected Disabled{}+    | inHaskellCode =+        if+            | -- end of a snippet+              stripSpaces l `startsWith` _texHaskellCodeEnd ->+                toTokens def{inText = True} ls (Text{someLines = l :| []} : result)+            | otherwise ->+                toTokens def{inHaskellCode} ls $+                  case r of+                    HaskellCode{..} -> (r{manyLines = l : manyLines} : rs)+                    _ -> errorExpected HaskellCode{}+    | stripSpaces l `startsWith` _texHaskellCodeStart =+        toTokens def{inHaskellCode = True} ls $+          HaskellCode{manyLines = []}+            : case r of+              Text{..} -> Text{someLines = l <| someLines} : rs+              _ -> Text{someLines = l :| []} : result+    | -- comment on a single line+      l `startsWith` texCommentSpace =+        let l' = dropLen texCommentSpace l+         in if+                | -- disable+                  l' `startsWith` _disable ->+                    toTokens def{inDisabled = True} ls (Disabled [] : result)+                | otherwise ->+                    toTokens def ls $+                      parseToken conf r l' lineNumber <> rs+    | inText =+        toTokens def{inText} ls $+          case r of+            Text{..} -> Text{someLines = l <| someLines} : rs+            _ -> errorExpected Text{}+    | -- a blank line+      T.null l =+        case r of+          Comment{someLines} -> toTokens def{inComment} ls (Comment{someLines = l <| someLines} : rs)+          _ -> toTokens def ls result+    | -- start of a text+      otherwise =+        toTokens def{inText = True} ls $ Text{someLines = l :| []} : result+   where+    errorExpected = errorExpectedToken lineNumber r+  toTokens _ _ res = res++-- | Convert 'Tokens' to @Literate Haskell@ code.+--+-- __Rules__+--+-- - Certain [assumptions]("Converter#assumptions") must hold for inputs.+--+-- - These are the relations between document blocks and tokens when the default 'Config' values are used.+--+--     - @'% LIMA_INDENT N'@ (@N@ is an 'Int') ~ 'Indent'.+--     - @'% LIMA_DEDENT'@ ~ 'Dedent'.+--     - Lines between and including @'% LIMA_DISABLE'@ and @'% LIMA_ENABLE'@ ~ 'Disabled'.+--+--         - There must be at least one nonempty line between these tags.+--+--     - Consecutive lines, either empty or starting with @'% '@ ~ 'Comment'.+--+--         @+--         % Hello,+--         % world!+--+--         % Hello,+--         % user!+--         @+--+--         - At least one line must have nonempty text after @'% '@+--+--     - Consecutive lines starting with @'> '@ ~ 'HaskellCode'.+--+--         @+--         > a4 = 4+--         > a2 = 2+--         @+--+--     - Other lines ~ 'Text'.+--+-- === __Example__+--+-- >>> pp $ lhsFromTokens def exampleNonTexTokens+-- % LIMA_INDENT 3+-- <BLANKLINE>+-- % LIMA_DISABLE+-- <BLANKLINE>+-- % -- What's the answer?+-- <BLANKLINE>+-- % LIMA_ENABLE+-- <BLANKLINE>+-- % LIMA_INDENT 1+-- <BLANKLINE>+-- % LIMA_INDENT 2+-- <BLANKLINE>+-- - Intermediate results+-- >   a = const 3+-- >   b = a 4+-- <BLANKLINE>+-- % LIMA_DEDENT+-- <BLANKLINE>+-- > answer = b * 14+-- <BLANKLINE>+-- % Hello from comments,+-- <BLANKLINE>+-- % world!+-- <BLANKLINE>+-- Hello from text,+-- world!+-- <BLANKLINE>+-- And from+-- here!+lhsFromTokens :: Config User -> Tokens -> T.Text+lhsFromTokens config tokens = mkFromTokens lhsFromTokens' config tokens++-- | Start a @Literate Haskell@ comment.+lhsComment :: T.Text+lhsComment = "%"++-- | Start a @Literate Haskell@ comment plus a space.+lhsCommentSpace :: T.Text+lhsCommentSpace = lhsComment <> " "++-- | Start a @Literate Haskell@ line of @Haskell@ code.+lhsHsCode :: T.Text+lhsHsCode = ">"++-- | Start a @Literate Haskell@ line of @Haskell@ code plus a space.+lhsHsCodeSpace :: T.Text+lhsHsCodeSpace = lhsHsCode <> " "++-- | Prepend start of a @TeX@ comment (@'% '@) to a 'T.Text'.+prependLhsComment :: T.Text -> T.Text+prependLhsComment l+  | l == T.empty = l+  | otherwise = texCommentSpace <> l++-- | Convert 'Tokens' to @Literate Haskell@ code.+--+-- Each 'Token' becomes a 'T.Text' in a list.+--+-- These 'T.Text's are concatenated in 'lhsFromTokens'.+lhsFromTokens' :: Config User -> Tokens -> [T.Text]+lhsFromTokens' (toInternalConfig -> Config{..}) blocks =+  dropEmpties $ reverse (T.intercalate "\n" . reverse <$> (fromTokens (Dedent : blocks) (0, [])))+ where+  fromTokens :: Tokens -> (Int, [[T.Text]]) -> [[T.Text]]+  fromTokens bs'@(_ : cur : bs) (curIndent, rs) =+    fromTokens (cur : bs) (translate curIndent bs' rs)+  fromTokens [_] (_, rs) = rs+  fromTokens _ _ = errorNotEnoughTokens Lhs+  translate curIndent (prev : cur : _) rs =+    case cur of+      Indent{..} -> (n,) $ [lhsCommentSpace <> _indent <> " " <> T.pack (show n)] : [] : rs+      Dedent -> (0,) $ [lhsCommentSpace <> _dedent] : [] : rs+      Disabled{..} -> (0,) $ [lhsCommentSpace <> _enable] : [] : (prependLhsComment <$> manyLines) : [] : [lhsCommentSpace <> _disable] : [] : rs+      HaskellCode{..} ->+        (curIndent,) $+          ((lhsHsCodeSpace <>) . indentN curIndent <$> manyLines)+            : ( case prev of+                  Text{} -> rs+                  _ -> [] : rs+              )+      Text{..} ->+        (curIndent,) $+          toList (lhsEscapeHash <$> someLines)+            : ( case prev of+                  HaskellCode{} -> rs+                  _ -> [] : rs+              )+      Comment{someLines = t :| ts} -> (curIndent, (prependLhsComment <$> t : ts) : [] : rs)+  translate _ _ _ = errorNotEnoughTokens Lhs++-- | Drop start of a @TeX@ comment from a 'T.Text'.+dropLhsComment :: Show a => T.Text -> a -> T.Text+dropLhsComment l lineNumber+  | l `startsWith` lhsCommentSpace = dropLen lhsCommentSpace l+  | l == T.empty = l+  | otherwise = error $ "The line " <> show lineNumber <> " must either be empty or start with '% '"++-- | Convert 'Tokens' to @Markdown@ code.+--+-- Inverse of 'lhsFromTokens'.+--+-- >>> (lhsToTokens def $ lhsFromTokens def exampleNonTexTokens) == exampleNonTexTokens+-- True+lhsToTokens :: Config User -> T.Text -> Tokens+lhsToTokens (toInternalConfig -> conf@Config{..}) xs = tokens+ where+  tokens = mkIntoTokens toTokens xs+  toTokens :: State -> [(Int, T.Text)] -> Tokens -> Tokens+  toTokens State{..} ((lineNumber, lhsUnescapeHash -> l) : ls) result@(r : rs)+    | inDisabled =+        if+            | -- enable+              l `startsWith` (lhsCommentSpace <> _enable) ->+                toTokens def ls result+            | -- copy lines+              otherwise ->+                toTokens def{inDisabled} ls $+                  case r of+                    Disabled{..} -> (r{manyLines = dropLhsComment l lineNumber : manyLines} : rs)+                    _ -> errorExpected Disabled{}+    | -- comment on a single line+      l `startsWith` lhsCommentSpace =+        let l' = dropLen lhsCommentSpace l+         in if+                | -- disable+                  l' `startsWith` _disable ->+                    toTokens def{inDisabled = True} ls (Disabled [] : result)+                | otherwise ->+                    toTokens def ls $+                      parseToken conf r l' lineNumber <> rs+    | -- start of a snippet+      l `startsWith` lhsHsCodeSpace =+        toTokens def{inHaskellCode = True} ls $+          let l' = dropLen lhsHsCodeSpace l+           in case r of+                HaskellCode{..} -> (r{manyLines = l' : manyLines} : rs)+                _ -> HaskellCode{manyLines = [l']} : result+    | inText =+        toTokens def{inText} ls $+          case r of+            Text{..} -> Text{someLines = l <| someLines} : rs+            _ -> errorExpected Text{}+    | -- a blank line+      T.null l =+        case r of+          Comment{someLines} -> toTokens def{inComment} ls (Comment{someLines = l <| someLines} : rs)+          _ -> toTokens def ls result+    | -- start of a text+      otherwise =+        toTokens def{inText = True} ls $ Text{someLines = l :| []} : result+   where+    errorExpected = error . errorExpectedToken lineNumber r+  toTokens _ _ res = res++-- | Convert 'Tokens' to @Markdown@ code.+--+-- __Rules__+--+-- - Certain [assumptions]("Converter#assumptions") must hold for inputs.+--+-- - These are the relations between document blocks and tokens when the default 'Config' values are used.+--+--     - @'<!-- LIMA_INDENT N --\>'@ (@N@ is an 'Int') ~ 'Indent'+--     - @'<!-- LIMA_DEDENT --\>'@ ~ 'Dedent'.+--     - Multiline comment+--       starting with @'<!-- LIMA_DISABLE\\n'@+--       and ending with @'\\nLIMA_ENABLE --\>'@  ~ 'Disabled'.+--+--         @+--         <!-- LIMA_DISABLE+--         a4 = 4+--         a2 = 2+--         LIMA_ENABLE --\>+--         @+--+--     - Multiline comments starting with @'<!-- {text}'@ where @{text}@ is nonempty text ~ 'Comment'.+--+--         @+--         <!-- line 1+--         line 2+--         --\>+--         @+--+--         - Consecutive 'Comment's are merged into a single 'Comment'.+--+--     - Possibly indented block starting with @\'```haskell\'@ and ending with @'```'@ ~ 'HaskellCode'.+--+--         @+--           ```haskell+--             a4 = 2+--           ```+--         @+--+--     - Other lines ~ 'Text'.+--+--         @+--         Hello, world!+--         @+--+-- === __Example__+--+-- >>> pp $ mdFromTokens def exampleNonTexTokens+--    <!-- LIMA_INDENT 3 -->+-- <BLANKLINE>+-- <!-- LIMA_DISABLE+-- <BLANKLINE>+-- -- What's the answer?+-- <BLANKLINE>+-- LIMA_ENABLE -->+-- <BLANKLINE>+--  <!-- LIMA_INDENT 1 -->+-- <BLANKLINE>+--   <!-- LIMA_INDENT 2 -->+-- <BLANKLINE>+-- - Intermediate results+-- <BLANKLINE>+--   ```haskell+--   a = const 3+--   b = a 4+--   ```+-- <BLANKLINE>+-- <!-- LIMA_DEDENT -->+-- <BLANKLINE>+-- ```haskell+-- answer = b * 14+-- ```+-- <BLANKLINE>+-- <!-- Hello from comments,+-- <BLANKLINE>+-- world!+-- -->+-- <BLANKLINE>+-- Hello from text,+-- world!+-- <BLANKLINE>+-- And from+-- here!+mdFromTokens :: Config User -> Tokens -> T.Text+mdFromTokens = mkFromTokens mdFromTokens'++-- | Open a @Markdown@ comment.+mdCommentOpen :: T.Text+mdCommentOpen = "<!--"++-- | Close a @Markdown@ comment.+mdCommentClose :: T.Text+mdCommentClose = "-->"++-- | Open a @Markdown@ comment plus a space.+mdCommentOpenSpace :: T.Text+mdCommentOpenSpace = mdCommentOpen <> " "++-- | A space plus close a @Markdown@ comment.+mdCommentCloseSpace :: T.Text+mdCommentCloseSpace = " " <> mdCommentClose++-- | Strip comment markers from a 'T.Text'.+stripMdComment :: T.Text -> T.Text+stripMdComment = stripEnds mdCommentOpenSpace mdCommentCloseSpace++-- | Convert 'Tokens' to @Haskell@ code.+--+-- Each 'Token' becomes a 'T.Text' in a list.+--+-- These 'T.Text's are concatenated in 'mdFromTokens'.+mdFromTokens' :: Config User -> Tokens -> [T.Text]+mdFromTokens' (toInternalConfig -> Config{..}) blocks =+  intersperse T.empty . reverse $ T.intercalate "\n" . reverse <$> fromTokens 0 blocks []+ where+  fromTokens :: Int -> Tokens -> [[T.Text]] -> [[T.Text]]+  fromTokens _ [] res = res+  fromTokens curIndent (b : bs) res =+    case b of+      Indent{..} -> fromTokens n bs ([indentN n $ mdCommentOpenSpace <> _indent <> " " <> T.pack (show n) <> mdCommentCloseSpace] : res)+      Dedent -> fromTokens 0 bs ([mdCommentOpenSpace <> _dedent <> mdCommentCloseSpace] : res)+      Disabled{..} -> fromTokens 0 bs ([[_enable <> mdCommentCloseSpace]] <> [manyLines] <> [[mdCommentOpenSpace <> _disable]] <> res)+      HaskellCode{..} -> fromTokens curIndent bs ((indentN curIndent <$> ([_mdHaskellCodeEnd] <> manyLines <> [_mdHaskellCodeStart])) : res)+      Text{..} -> fromTokens curIndent bs (toList someLines : res)+      Comment{someLines = t :| ts} ->+        let ts' = t : ts+         in fromTokens curIndent bs $ [mdCommentClose] <> init ts' <> [mdCommentOpenSpace <> last ts'] : res++-- | Show the name of a constructor.+constructorName :: Data a => a -> String+constructorName x = showConstr (toConstr x)++-- | Remove empty lines from the beginning and the end of a list.+stripEmpties :: [T.Text] -> [T.Text]+stripEmpties = stripList T.empty++dropEmpties :: [T.Text] -> [T.Text]+dropEmpties = dropWhile (== T.empty)++-- | Check if a line without leading spaces is surrounded by the given 'T.Text's.+isEnclosedWith :: T.Text -> T.Text -> T.Text -> Bool+isEnclosedWith start end (stripSpaces -> x) = x `startsWith` start && x `endsWith` end++-- | Check if a line is a @Markdown@ comment.+isMdComment :: T.Text -> Bool+isMdComment = isEnclosedWith mdCommentOpenSpace mdCommentCloseSpace++-- | Count leading spaces in a 'T.Text'.+countSpaces :: T.Text -> Int+countSpaces x = T.length $ T.takeWhile (== ' ') x++-- | Strip empty lines an leading spaces in 'Tokens'.+--+-- - Remove empty lines in 'Tokens'.+-- - Shift lines in 'HaskellCode' to the left by the minimal number of leading spaces in nonempty lines.+--+-- >>> pp exampleNonTexTokens'+-- [+--   Indent {n = 3},+--   Disabled {manyLines = ["-- What's the answer?"]},+--   Indent {n = 1},+--   Indent {n = 2},+--   Text {someLines = "- Intermediate results" :| []},+--   HaskellCode {manyLines = ["   b = a 4","   a = const 3"]},+--   Dedent,+--   HaskellCode {manyLines = ["answer = b * 14"]},+--   Comment {someLines = "Hello from comments," :| []},+--   Comment {someLines = "world!" :| []},+--   Text {someLines = "world!" :| ["Hello from text,"]},+--   Text {someLines = "here!" :| ["And from"]}+-- ]+--+-- >>> pp $ stripTokens exampleNonTexTokens'+-- [+--   Indent {n = 3},+--   Disabled {manyLines = ["-- What's the answer?"]},+--   Indent {n = 1},+--   Indent {n = 2},+--   Text {someLines = "- Intermediate results" :| []},+--   HaskellCode {manyLines = ["b = a 4","a = const 3"]},+--   Dedent,+--   HaskellCode {manyLines = ["answer = b * 14"]},+--   Comment {someLines = "Hello from comments," :| []},+--   Comment {someLines = "world!" :| []},+--   Text {someLines = "world!" :| ["Hello from text,"]},+--   Text {someLines = "here!" :| ["And from"]}+-- ]+stripTokens :: Tokens -> Tokens+stripTokens xs =+  ( \case+      Disabled{..} -> Disabled{manyLines = stripEmpties manyLines}+      HaskellCode{..} ->+        let ls = stripEmpties manyLines+         in HaskellCode{manyLines = T.drop (minimum (countSpaces <$> filter (not . T.null) ls)) <$> ls}+      Text{..} -> Text{someLines = fromList $ stripEmpties (toList someLines)}+      Comment{..} -> Comment{someLines = fromList $ stripEmpties (toList someLines)}+      x -> x+  )+    <$> xs++-- | Convert 'Tokens' to @Markdown@ code.+--+-- Inverse of 'mdFromTokens'.+--+-- >>> (mdToTokens def $ mdFromTokens def exampleNonTexTokens) == exampleNonTexTokens+-- True+mdToTokens :: Config User -> T.Text -> Tokens+mdToTokens (toInternalConfig -> conf@Config{..}) xs = tokens+ where+  tokens = mkIntoTokens toTokens xs+  toTokens :: State -> [(Int, T.Text)] -> Tokens -> Tokens+  toTokens State{..} ((lineNumber, l) : ls) res@(r : rs)+    | inDisabled =+        -- enable+        if l `startsWith` (_enable <> mdCommentCloseSpace)+          then toTokens def ls res+          else -- copy lines+          toTokens def{inDisabled} ls $+            case r of+              Disabled{..} -> (r{manyLines = l : manyLines} : rs)+              _ -> errorExpected Disabled{}+    | inComment =+        if l `startsWith` mdCommentClose+          then -- finish comment+            toTokens def ls res+          else -- copy lines+          toTokens def{inComment} ls $+            case r of+              Comment{..} -> (r{someLines = l <| someLines} : rs)+              _ -> errorExpected Comment{}+    | inHaskellCode =+        if stripSpaces l `startsWith` _mdHaskellCodeEnd+          then -- finish snippet+            toTokens def ls res+          else -- copy lines+          toTokens def{inHaskellCode} ls $+            case r of+              HaskellCode{..} -> (r{manyLines = l : manyLines} : rs)+              _ -> errorExpected HaskellCode{}+    -- Doesn't matter if in text++    | -- comment on a single line+      isMdComment l =+        toTokens def ls $ parseToken conf r (stripMdComment l) lineNumber <> rs+    | -- start of a comment on multiple lines+      l `startsWith` mdCommentOpenSpace =+        let l' = dropLen mdCommentOpenSpace l+         in if+                | l' == _disable ->+                    toTokens def{inDisabled = True} ls (Disabled [] : res)+                | T.null l' -> error $ errorEmptyCommentAt lineNumber+                | otherwise ->+                    toTokens def{inComment = True} ls $+                      Comment (l' :| []) : res+    | -- start of a haskell snippet+      stripSpaces l `startsWith` _mdHaskellCodeStart =+        toTokens def{inHaskellCode = True} ls (HaskellCode [] : res)+    -- Again matters if in a text+    | inText =+        toTokens def{inText} ls $+          case r of+            Text{..} -> Text{someLines = l <| someLines} : rs+            _ -> errorExpected Text{}+    | otherwise =+        if+            | T.null l ->+                -- skip+                toTokens def ls res+            | otherwise ->+                -- start a text+                toTokens def{inText = True} ls $ Text{someLines = l :| []} : res+   where+    errorExpected = error . errorExpectedToken lineNumber r+  toTokens _ _ res = res++-- | Convert 'Tokens' to @Haskell@ code.+--+-- __Rules__+--+-- - Certain [assumptions]("Converter#assumptions") must hold for inputs.+--+-- - These are the relations between document blocks and tokens when the default 'Config' values are used.+--+--     - @'{- LIMA_INDENT N -}'@ (@N@ is an 'Int') ~ 'Indent'.+--     - @'{- LIMA_DEDENT -}'@ ~ 'Dedent'.+--     - Lines between and including @'{- LIMA_DISABLE -}'@ and @'{- LIMA_ENABLE -}'@ ~ 'Disabled'.+--+--     - Multiline comment starting with @'{-\\n'@ ~ 'Text'.+--+--         @+--         {-+--         line 1+--         -}+--         @+--+--         - Consecutive 'Text's are merged into a single 'Text'.+--         - There must be at list one nonempty line inside this comment.+--+--     - Multiline comment starting with @'{- '@ where @<text>@ is nonempty text ~ 'Comment'.+--+--         @+--         {- line 1+--         line 2+--         -}+--         @+--+--         - Consecutive 'Comment's are merged into a single 'Comment'.+--+--     - Other lines ~ 'HaskellCode'.+--+--         @+--         a = 42+--         @+--+-- === __Example__+--+-- >>> pp $ hsFromTokens def exampleNonTexTokens+-- {- LIMA_INDENT 3 -}+-- <BLANKLINE>+-- {- LIMA_DISABLE -}+-- <BLANKLINE>+-- -- What's the answer?+-- <BLANKLINE>+-- {- LIMA_ENABLE -}+-- <BLANKLINE>+-- {- LIMA_INDENT 1 -}+-- <BLANKLINE>+-- {- LIMA_INDENT 2 -}+-- <BLANKLINE>+-- {-+-- - Intermediate results+-- -}+-- <BLANKLINE>+-- a = const 3+-- b = a 4+-- <BLANKLINE>+-- {- LIMA_DEDENT -}+-- <BLANKLINE>+-- answer = b * 14+-- <BLANKLINE>+-- {- Hello from comments,+-- <BLANKLINE>+-- world!+-- -}+-- <BLANKLINE>+-- {-+-- Hello from text,+-- world!+-- <BLANKLINE>+-- And from+-- here!+-- -}+hsFromTokens :: Config User -> Tokens -> T.Text+hsFromTokens = mkFromTokens hsFromTokens'++-- | Open a @Haskell@ multi-line comment.+hsCommentOpen :: T.Text+hsCommentOpen = "{-"++-- | Open a @Haskell@ multi-line comment plus a space.+hsCommentOpenSpace :: T.Text+hsCommentOpenSpace = hsCommentOpen <> " "++-- | Close a @Haskell@ multi-line comment.+hsCommentClose :: T.Text+hsCommentClose = "-}"++-- | A space plus close a @Haskell@ multi-line comment.+hsCommentCloseSpace :: T.Text+hsCommentCloseSpace = " " <> hsCommentClose++-- | Convert 'Tokens' to @Haskell@ code.+--+-- Each 'Token' becomes a 'T.Text' in a list.+--+-- These 'T.Text's are concatenated in 'hsFromTokens'.+hsFromTokens' :: Config User -> Tokens -> [T.Text]+hsFromTokens' (toInternalConfig -> Config{..}) blocks =+  intersperse T.empty . reverse $ T.intercalate "\n" . reverse <$> toHs blocks []+ where+  toHs :: Tokens -> [[T.Text]] -> [[T.Text]]+  toHs [] res = res+  toHs (b : bs) res =+    toHs bs $+      case b of+        Indent{..} -> [hsCommentOpenSpace <> _indent <> " " <> T.pack (T.show n) <> hsCommentCloseSpace] : res+        Dedent -> [hsCommentOpenSpace <> _dedent <> hsCommentCloseSpace] : res+        Disabled{..} ->+          [[hsCommentOpenSpace <> _enable <> hsCommentCloseSpace]]+            <> [manyLines]+            <> [[hsCommentOpenSpace <> _disable <> hsCommentCloseSpace]]+            <> res+        HaskellCode{..} -> manyLines : res+        Text{..} -> [hsCommentClose] <> toList someLines <> [hsCommentOpen] : res+        Comment{someLines = t :| ts} ->+          let ts' = t : ts+           in [hsCommentClose] <> init ts' <> [hsCommentOpenSpace <> last ts'] : res++-- | Drop leading spaces and drop at each end of a 'T.Text' the number of characters as in the supplied prefix and suffix.+stripHsComment :: T.Text -> T.Text+stripHsComment = stripEnds hsCommentOpenSpace hsCommentCloseSpace++-- | Check if a line without leading zeros is a multi-line @Haskell@ comment+isHsComment :: T.Text -> Bool+isHsComment = isEnclosedWith hsCommentOpenSpace hsCommentCloseSpace++-- | Show error with line number for a token.+-- errorEmptyCommentAt :: Int -> String+errorEmptyCommentAt :: Show a1 => a1 -> a2+errorEmptyCommentAt lineNumber =+  error $+    ("Expected a 'Comment' at line " <> show lineNumber <> ".\n\n")+      <> "However, there are no characters after '{- '.\n\n"+      <> "Please, write there something after '{- '."++-- | Convert 'Tokens' to @Haskell@ code.+--+-- Inverse of 'hsFromTokens'.+--+-- >>> (hsToTokens def $ hsFromTokens def exampleNonTexTokens) == exampleNonTexTokens+-- True+hsToTokens :: Config User -> T.Text -> Tokens+hsToTokens (toInternalConfig -> conf@Config{..}) xs = tokens+ where+  tokens = mkIntoTokens toTokens xs+  toTokens :: State -> [(Int, T.Text)] -> Tokens -> Tokens+  toTokens State{..} ((lineNumber, l) : ls) res@(r : rs)+    | inText =+        if+            | l `startsWith` hsCommentClose ->+                case r of+                  Text{someLines}+                    | stripEmpties (toList someLines) == [] ->+                        error $+                          ("No text in a 'Text' token ending at line " <> show lineNumber <> ".\n\n")+                            <> "Please, write some text between '{-\\n' and '\\n-}'."+                    | otherwise -> toTokens def ls res+                  _ -> errorExpected Text{}+            | otherwise ->+                -- copy lines+                toTokens (def{inText}) ls $+                  case r of+                    Text{..} -> (r{someLines = l <| someLines} : rs)+                    _ -> errorExpected Text{}+    | inDisabled =+        if+            | isHsComment l && stripHsComment l `startsWith` _enable ->+                -- enable+                toTokens def ls res+            | otherwise ->+                -- copy lines+                toTokens def{inDisabled} ls $+                  case r of+                    Disabled{..} -> (r{manyLines = l : manyLines} : rs)+                    _ -> errorExpected Disabled{}+    | inComment =+        if+            | -- finish comment+              l `startsWith` hsCommentClose ->+                case r of+                  Comment{someLines}+                    | stripEmpties (toList someLines) == [] ->+                        error $+                          ("No text in a 'Comment' token ending at line " <> show lineNumber <> ".\n\n")+                            <> "Please, write some text between '{- ' and '\\n-}'."+                    | otherwise -> toTokens def ls res+                  _ -> errorExpected Comment{}+            | -- copy lines+              otherwise ->+                toTokens def{inComment} ls $+                  case r of+                    Comment{..} -> (r{someLines = l <| someLines} : rs)+                    _ -> errorExpected Comment{}+    -- Doesn't matter if in a snippet++    | -- start of text+      l == hsCommentOpen =+        toTokens def{inText = True} ls (Text{someLines = T.empty :| []} : res)+    | -- comment on a single line+      isHsComment l =+        let l' = stripHsComment l+         in if+                | l' `startsWith` _disable -> toTokens def{inDisabled = True} ls (Disabled [] : res)+                -- \| null l' -> error+                | otherwise -> toTokens def ls $ parseToken conf r l' lineNumber <> rs+    | -- start of a comment on multiple lines+      l `startsWith` hsCommentOpenSpace =+        let l' = dropLen hsCommentOpenSpace l+         in if+                | T.null l' -> errorEmptyCommentAt lineNumber+                | otherwise ->+                    toTokens def{inComment = True} ls $+                      Comment (l' :| []) : res+    -- Again matters if in a snippet+    | inHaskellCode =+        toTokens def{inHaskellCode} ls $+          case r of+            HaskellCode{..} -> HaskellCode{manyLines = l : manyLines} : rs+            _ -> errorExpected HaskellCode{}+    | -- a blank line+      T.null l =+        toTokens def ls res+    | -- start of a snippet+      otherwise =+        toTokens def{inHaskellCode = True} ls (HaskellCode [l] : res)+   where+    errorExpected = error . errorExpectedToken lineNumber r+  toTokens _ _ res = res++-- | Show a 'Format' as a file extension.+--+-- >>>showFormatExtension Lhs+-- "lhs"+showFormatExtension :: Format -> String+showFormatExtension = \case+  Hs -> "hs"+  Md -> "md"+  Lhs -> "lhs"+  TeX -> "tex"++-- | Show a 'Format' as a full name.+--+-- >>>showFormatName Lhs+-- "Literate Haskell"+showFormatName :: Format -> String+showFormatName = \case+  Hs -> "Haskell"+  Md -> "Markdown"+  Lhs -> "Literate Haskell"+  TeX -> "TeX"
+ test/Conversions/Main.hs view
@@ -0,0 +1,190 @@+{-# OPTIONS_GHC -fplugin Debug.Breakpoint #-}++import Converter (Config, Format (..), Token (..), Tokens, User, def, exampleNonTexTokens, exampleTexTokens, normalizeTokens, selectFromTokens, selectToTokens, showFormatExtension, showFormatName, stripEmpties, texHaskellCodeEnd, texHaskellCodeStart, toInternalConfig)+import Data.List.NonEmpty (NonEmpty (..))+import Data.String.Interpolate (i)+import Data.Text qualified as T+import Data.Text.IO qualified as T+import Data.Text.Lazy qualified as TL+import Hedgehog (Gen, MonadGen, MonadTest, Property, property, tripping)+import Hedgehog.Gen qualified as Gen+import Hedgehog.Range qualified as Range+import Lens.Micro ((^.))+import System.Directory (createDirectoryIfMissing)+import System.IO (IOMode (WriteMode), withFile)+import Test.Tasty (TestTree, defaultMain, testGroup, withResource)+import Test.Tasty.HUnit (assertEqual, testCase)+import Test.Tasty.Hedgehog (testProperty)+import Text.Pretty.Simple (CheckColorTty (..), OutputOptions (..), StringOutputStyle (Literal), defaultOutputOptionsDarkBg, pHPrintOpt, pShow, pShowNoColor, pStringNoColor)++testDir :: String+testDir = "testdata"++main :: IO ()+main =+  defaultMain $+    testGroup+      "Top"+      [ testWrite Md+      , testWrite TeX+      , testTripping TeX+      , testTripping Md+      ]++-- TODO test initial haskell code indentation is the same as after parsing+-- so, generate lines with indentation relative to the previous indent token++-- TODO test no newline as the first line in fromTokens++-- TODO no newlines in the generated lines++--++selectDialectName :: Format -> String+selectDialectName = \case+  TeX -> showFormatName TeX+  _ -> "Non-" <> showFormatName TeX++testDialectWrite :: FilePath -> Tokens -> Format -> TestTree+testDialectWrite dir tokens format =+  testCase [i|Write #{showFormatName format} code|] do+    let text = selectFromTokens def format tokens+    T.writeFile [i|#{dir}/test.#{showFormatExtension format}|] text+    assertEqual "Roundtrip" tokens (selectToTokens def format text)++writeTokens :: FilePath -> Tokens -> TestTree+writeTokens dir tokens = testCase "Write tokens to a file" $+  withFile [i|#{dir}/tokens.hs|] WriteMode $+    \h ->+      pHPrintOpt+        CheckColorTty+        ( defaultOutputOptionsDarkBg+            { outputOptionsStringStyle = Literal+            }+        )+        h+        tokens++testFormatsWrite :: FilePath -> [Format] -> Tokens -> [TestTree]+testFormatsWrite dir formats tokens =+  (testDialectWrite dir tokens <$> formats) <> [writeTokens dir tokens]++selectTokens :: Format -> Tokens+selectTokens = \case+  TeX -> exampleTexTokens+  _ -> exampleNonTexTokens++selectFormats :: Format -> [Format]+selectFormats = \case+  TeX -> [Hs, Md, Lhs, TeX]+  _ -> [Hs, Md, Lhs]++testWrite :: Format -> TestTree+testWrite format =+  let dir = [i|#{testDir}/#{showFormatExtension format}|]+   in withResource (createDirectoryIfMissing True dir) pure $+        const $+          testGroup [i|Using #{selectDialectName format} tokens|] $+            (testFormatsWrite dir (selectFormats format) (selectTokens format))++alphabet :: MonadGen m => m Char+alphabet = Gen.alphaNum++genNonEmpty :: Gen (NonEmpty T.Text)+genNonEmpty = Gen.nonEmpty (Range.constant 1 5) genLine++genLine :: Gen T.Text+genLine = do+  indent <- Gen.text (Range.constant 0 5) (pure ' ')+  contents <- Gen.text (Range.constant 1 5) alphabet+  pure $ indent <> contents++genLines :: Gen [T.Text]+genLines = Gen.list (Range.constant 1 5) genLine++-- Tokens++genIndent :: Gen Token+genIndent = do+  n <- Gen.int (Range.constant 0 10)+  pure Indent{..}++genDedent :: Gen Token+genDedent = pure Dedent++genComment :: Gen Token+genComment = do+  someLines <- genNonEmpty+  pure $ Comment{someLines}++genText :: Gen Token+genText = do+  someLines <- genNonEmpty+  pure Text{..}++genNonDisabled :: (?genCode :: GenCode) => Gen Tokens+genNonDisabled =+  Gen.choice $+    [?genCode] <> (((: []) <$>) <$> [genComment, genText, genIndent, genDedent])++genDisabled :: Env => Gen Token+genDisabled = do+  tokens <- genNonDisabled+  let manyLines = reverse $ T.lines $ selectFromTokens ?config ?format tokens+  pure $ Disabled{manyLines}++genTokensSublist :: Env => Gen Tokens+genTokensSublist =+  Gen.choice $ [genNonDisabled, (: []) <$> genDisabled]++genTokens :: Env => Gen Tokens+genTokens = do+  subLists <- Gen.list (Range.singleton 1000) genTokensSublist+  pure $ normalizeTokens $ concat subLists++-- Code tokens from tex files can only be recognized in a specific sequence+-- That's why, they're generated in a list+texGenCode :: (?config :: Config User) => Gen [Token]+texGenCode = do+  let config = toInternalConfig ?config+  manyLines <- genLines+  let manyLines' = if null (stripEmpties manyLines) then ["a"] else manyLines+  pure $+    [ Text{someLines = config ^. texHaskellCodeStart :| []}+    , HaskellCode{manyLines = manyLines'}+    , Text{someLines = config ^. texHaskellCodeEnd :| []}+    ]++-- Code tokens from tex files can only be recognized in a specific sequence+-- That's why, they're generated in a list+notTexGenCode :: Gen [Token]+notTexGenCode = do+  manyLines <- genLines+  let manyLines' = if null (stripEmpties manyLines) then ["a"] else manyLines+  pure [HaskellCode{manyLines = manyLines'}]++-- Tripping+trippingTokens :: (MonadTest m, ?config :: Config User, ?format :: Format) => Tokens -> m ()+trippingTokens tokens =+  tripping tokens (selectFromTokens ?config ?format) (Just . selectToTokens ?config ?format)++testTrippingTokens :: Env => Property+testTrippingTokens = property do+  tokens <- Gen.sample $ genTokens+  -- breakpointM+  trippingTokens tokens++type GenCode = Gen [Token]++type Env = (?config :: Config User, ?format :: Format, ?genCode :: GenCode)++selectGenCode :: Format -> ((?config :: Config User) => GenCode)+selectGenCode = \case+  TeX -> texGenCode+  _ -> notTexGenCode++testTripping :: Format -> TestTree+testTripping format =+  let ?config = def; ?format = format+   in let ?genCode = selectGenCode format+       in testProperty [i|Roundtrip #{selectDialectName format} tokens for all formats|] $ testTrippingTokens
+ test/Doctests/Main.hs view
@@ -0,0 +1,5 @@+import System.Environment (getArgs)+import Test.DocTest (mainFromCabal)++main :: IO ()+main = mainFromCabal "lima" =<< getArgs
+ test/HsToMd/Main.hs view
@@ -0,0 +1,5 @@+import Converter (Format (..), convertTo, def)+import Data.Text.IO qualified as T++main :: IO ()+main = T.readFile "README.hs" >>= T.writeFile "README.md" . (Hs `convertTo` Md) def
− test/Main.hs
@@ -1,50 +0,0 @@-{-# LANGUAGE QuasiQuotes #-}-{-# 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.String.Interpolate (i)-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 = (\x -> [i|#{testDir}/hs/input#{x}.hs|]) <$> [0 :: Int]-      pathsMd1 = (++ ".md") <$> pathsHs1-      pathsHs2 = (++ ".hs") <$> pathsMd1-  Config{..} <--    decodeFileThrow [i|#{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 = (\x -> [i|#{testDir}/lhs/input#{x}.lhs|]) <$> [0 :: Int, 1]-      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
+ test/MdToHs/Main.hs view
@@ -0,0 +1,5 @@+import Converter (Format (..), convertTo, def)+import Data.Text.IO qualified as T++main :: IO ()+main = T.readFile "README.md" >>= T.writeFile "README.hs" . (Md `convertTo` Hs) def
− testdata/config/lima.yaml
@@ -1,4 +0,0 @@-hs-md:-  special-comments:-    - "first special comment"-    - "second special comment"
− testdata/hs/input0.hs
@@ -1,151 +0,0 @@-{--# Top heading--- Only multi-line comments are supported for Markdown--- Haskell code snippet (use `hs`)--    ```hs-    "1+4+2+3"-    ```--- Console code snippet--```console-"1*2*3*4"-```--1. what's in comments is written as is-2. Haskell code becomes haskell code snippets--- There should be a line between a multi-line comment and Haskell code--- A Haskell snippet starts with a non-empty line that is not a part of a multi-line comment--- Magic comments will be skipped--- Haskell function docs won't--- If there's a single line btw Haskell code and a comment, this line is removed--the following two comments will be commented out due to a config--}--{- first special comment -}--{- second special comment -}---- before a magic comment--{- FOURMOLU_DISABLE -}---- after a magic comment-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# OPTIONS_GHC -Wno-missing-kind-signatures #-}--{- FOURMOLU_ENABLE -}---- this comment will go into the Haskell snippet-module Main where -- a comment after--a1 :: Integer-a1 = 4--{--This comment will divide Haskell snippets--}---- Imports--a2 :: Integer-a2 = 2--{--## Second-level heading--some text--### Third-level heading--}---- Text one--- this multiline comment should separate this line and the line after the comment-{--}--- Text two should be ignored--{- LIMA_DISABLE -}--- Text two to ignore-some :: String-some = "code to ignore"--{- LIMA_ENABLE -}--{--- Indented block--}--{- LIMA_INDENT 2 -}---- Text three---- This comment will go into a Haskell snippet---- | This doc will go into a Haskell snippet-class (Monoid (m a)) => Reducible m a where-  op :: m a -> Char--  -- | this too-  constr :: m a -> a -> m a--  -- | and this one-  get :: m a -> m a -> a--{--don't put anything outside and after this comment like in `{- -} -- hey`--}---- a comment that will go into a snippet--{- LIMA_DEDENT -}--{- FOURMOLU_DISABLE -}--{-| some -doc that will be in a snippet -}-main :: IO ()-main = print "hi!"--{- FOURMOLU_ENABLE -}--{--<b name="fn_laws">1</b> <- Some html--{- LIMA_DISABLE -}--in comments should read like {- LIMA_DISABLE -}--}--{- LIMA_DISABLE -}---- Some text--{- LIMA_ENABLE -}---- shouldn't be ignored--{--this LIMA_ENABLE reads like a comment because there's no corresponding-preceding LIMA_DISABLE--1. listing--}--{- LIMA_INDENT 4 -}---- where's lorem ipsum blah?--{- LIMA_DEDENT -}---- final comment
− testdata/hs/input0.hs.md
@@ -1,154 +0,0 @@-# Top heading--- Only multi-line comments are supported for Markdown--- Haskell code snippet (use `hs`)--    ```hs-    "1+4+2+3"-    ```--- Console code snippet--```console-"1*2*3*4"-```--1. what's in comments is written as is-2. Haskell code becomes haskell code snippets--- There should be a line between a multi-line comment and Haskell code--- A Haskell snippet starts with a non-empty line that is not a part of a multi-line comment--- Magic comments will be skipped--- Haskell function docs won't--- If there's a single line btw Haskell code and a comment, this line is removed--the following two comments will be commented out due to a config-<!-- first special comment -->-<!-- second special comment -->--```haskell--- before a magic comment-```--<!-- FOURMOLU_DISABLE -->--```haskell--- after a magic comment-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# OPTIONS_GHC -Wno-missing-kind-signatures #-}-```--<!-- FOURMOLU_ENABLE -->--```haskell--- this comment will go into the Haskell snippet-module Main where -- a comment after--a1 :: Integer-a1 = 4-```--This comment will divide Haskell snippets--```haskell--- Imports--a2 :: Integer-a2 = 2-```--## Second-level heading--some text--### Third-level heading--```haskell--- Text one--- this multiline comment should separate this line and the line after the comment-{--}--- Text two should be ignored-```-<!-- LIMA_DISABLE--- Text two to ignore-some :: String-some = "code to ignore"--LIMA_ENABLE -->--- Indented block--  <!-- LIMA_INDENT 2 -->--  ```haskell-  -- Text three-  -  -- This comment will go into a Haskell snippet-  -  -- | This doc will go into a Haskell snippet-  class (Monoid (m a)) => Reducible m a where-    op :: m a -> Char-  -    -- | this too-    constr :: m a -> a -> m a-  -    -- | and this one-    get :: m a -> m a -> a-  ```--don't put anything outside and after this comment like in `{- -} -- hey`--  ```haskell-  -- a comment that will go into a snippet-  ```--  <!-- LIMA_DEDENT -->-<!-- FOURMOLU_DISABLE -->--```haskell-{-| some -doc that will be in a snippet -}-main :: IO ()-main = print "hi!"-```--<!-- FOURMOLU_ENABLE -->--<b name="fn_laws">1</b> <- Some html--{- LIMA_DISABLE -}--in comments should read like {- LIMA_DISABLE -}-<!-- LIMA_DISABLE---- Some text--LIMA_ENABLE -->--```haskell--- shouldn't be ignored-```--this LIMA_ENABLE reads like a comment because there's no corresponding-preceding LIMA_DISABLE--1. listing--    <!-- LIMA_INDENT 4 -->--    ```haskell-    -- where's lorem ipsum blah?-    ```--    <!-- LIMA_DEDENT -->--```haskell--- final comment-```
− testdata/hs/input0.hs.md.hs
@@ -1,151 +0,0 @@-{--# Top heading--- Only multi-line comments are supported for Markdown--- Haskell code snippet (use `hs`)--    ```hs-    "1+4+2+3"-    ```--- Console code snippet--```console-"1*2*3*4"-```--1. what's in comments is written as is-2. Haskell code becomes haskell code snippets--- There should be a line between a multi-line comment and Haskell code--- A Haskell snippet starts with a non-empty line that is not a part of a multi-line comment--- Magic comments will be skipped--- Haskell function docs won't--- If there's a single line btw Haskell code and a comment, this line is removed--the following two comments will be commented out due to a config--}--{- first special comment -}--{- second special comment -}---- before a magic comment--{- FOURMOLU_DISABLE -}---- after a magic comment-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# OPTIONS_GHC -Wno-missing-kind-signatures #-}--{- FOURMOLU_ENABLE -}---- this comment will go into the Haskell snippet-module Main where -- a comment after--a1 :: Integer-a1 = 4--{--This comment will divide Haskell snippets--}---- Imports--a2 :: Integer-a2 = 2--{--## Second-level heading--some text--### Third-level heading--}---- Text one--- this multiline comment should separate this line and the line after the comment-{--}--- Text two should be ignored--{- LIMA_DISABLE -}--- Text two to ignore-some :: String-some = "code to ignore"--{- LIMA_ENABLE -}--{--- Indented block--}--{- LIMA_INDENT 2 -}---- Text three---- This comment will go into a Haskell snippet---- | This doc will go into a Haskell snippet-class (Monoid (m a)) => Reducible m a where-  op :: m a -> Char--  -- | this too-  constr :: m a -> a -> m a--  -- | and this one-  get :: m a -> m a -> a--{--don't put anything outside and after this comment like in `{- -} -- hey`--}---- a comment that will go into a snippet--{- LIMA_DEDENT -}--{- FOURMOLU_DISABLE -}--{-| some -doc that will be in a snippet -}-main :: IO ()-main = print "hi!"--{- FOURMOLU_ENABLE -}--{--<b name="fn_laws">1</b> <- Some html--{- LIMA_DISABLE -}--in comments should read like {- LIMA_DISABLE -}--}--{- LIMA_DISABLE -}---- Some text--{- LIMA_ENABLE -}---- shouldn't be ignored--{--this LIMA_ENABLE reads like a comment because there's no corresponding-preceding LIMA_DISABLE--1. listing--}--{- LIMA_INDENT 4 -}---- where's lorem ipsum blah?--{- LIMA_DEDENT -}---- final comment
− testdata/lhs/input0.lhs
@@ -1,170 +0,0 @@- # Dillinger--[![N|Solid](https://cldup.com/dTxpPi9lDf.thumb.png)](https://nodesource.com/products/nsolid)--[![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master)](https://travis-ci.org/joemccann/dillinger)--Dillinger is a cloud-enabled, mobile-ready, offline-storage, AngularJS powered HTML5 Markdown editor.--- Type some Markdown on the left-- See HTML in the right-- Magic-- ## New Features--- Import a HTML file and watch it magically convert to Markdown-- Drag and drop images (requires your Dropbox account be linked)--You can also:--- Import and save files from GitHub, Dropbox, Google Drive and One Drive-- Drag and drop markdown and HTML files into Dillinger-- Export documents as Markdown, HTML and PDF--Markdown is a lightweight markup language based on the formatting conventions that people naturally use in email.  As [John Gruber] writes on the [Markdown site][df1]-- > The overriding design goal for Markdown's- > formatting syntax is to make it as readable- > as possible. The idea is that a- > Markdown-formatted document should be- > publishable as-is, as plain text, without- > looking like it's been marked up with tags- > or formatting instructions.--This text you see here is *actually* written in Markdown! To get a feel for Markdown's syntax, type some text into the left window and watch the results in the right.-- ### Tech--Dillinger uses a number of open source projects to work properly:--- [AngularJS] - HTML enhanced for web apps!-- [Ace Editor] - awesome web-based text editor-- [markdown-it] - Markdown parser done right. Fast and easy to extend.-- [Twitter Bootstrap] - great UI boilerplate for modern web apps-- [node.js] - evented I/O for the backend-- [Express] - fast node.js network app framework [@tjholowaychuk]-- [Gulp] - the streaming build system-- [Breakdance](https://breakdance.github.io/breakdance/) - HTML to Markdown converter-- [jQuery] - duh--And of course Dillinger itself is open source with a [public repository][dill]- on GitHub.-- ### Installation--Dillinger requires [Node.js](https://nodejs.org/) v4+ to run.--Install the dependencies and devDependencies and start the server.--< cd dillinger-< npm install -d-< node app--For production environments...--< npm install --production-< NODE_ENV=production node app-- ### Plugins--Dillinger is currently extended with the following plugins. Instructions on how to use them in your own application are linked below.--| Plugin | README |-| ------ | ------ |-| Dropbox | [plugins/dropbox/README.md][PlDb] |-| GitHub | [plugins/github/README.md][PlGh] |-| Google Drive | [plugins/googledrive/README.md][PlGd] |-| OneDrive | [plugins/onedrive/README.md][PlOd] |-| Medium | [plugins/medium/README.md][PlMe] |-| Google Analytics | [plugins/googleanalytics/README.md][PlGa] |-- ### Development--Want to contribute? Great!--Dillinger uses Gulp + Webpack for fast developing.-Make a change in your file and instantaneously see your updates!--Open your favorite Terminal and run these commands.--First Tab:--< node app--Second Tab:--< gulp watch--(optional) Third:--< karma test-- #### Building for source--For production release:--< gulp build --prod--Generating pre-built zip archives for distribution:--< gulp build dist --prod-- #### Docker--Dillinger is very easy to install and deploy in a Docker container.--By default, the Docker will expose port 8080, so change this within the Dockerfile if necessary. When ready, simply use the Dockerfile to build the image.--< cd dillinger-< docker build -t joemccann/dillinger:${package.json.version} .--This will create the dillinger image and pull in the necessary dependencies. Be sure to swap out `${package.json.version}` with the actual version of Dillinger.--Once done, run the Docker image and map the port to whatever you wish on your host. In this example, we simply map port 8000 of the host to port 8080 of the Docker (or whatever port was exposed in the Dockerfile):--< docker run -d -p 8000:8080 --restart="always" <youruser>/dillinger:${package.json.version}--Verify the deployment by navigating to your server address in your preferred browser.--< 127.0.0.1:8000-- #### Kubernetes + Google Cloud--See [KUBERNETES.md](https://github.com/joemccann/dillinger/blob/master/KUBERNETES.md)--> module Main where->  -> main = putStrLn "HelloWorld"-- ### Todos--- Write MORE Tests-- Add Night Mode-- ### License--MIT--**Free Software, Hell Yeah!**--[//]: # (These are reference links used in the body of this note and get stripped out when the markdown processor does its job. There is no need to format nicely because it shouldn't be seen. Thanks SO - http://stackoverflow.com/questions/4823468/store-comments-in-markdown-syntax)--   [dill]: <https://github.com/joemccann/dillinger>-   [john gruber]: <http://daringfireball.net>-   [df1]: <http://daringfireball.net/projects/markdown/>-   [markdown-it]: <https://github.com/markdown-it/markdown-it>-   [Ace Editor]: <http://ace.ajax.org>-   [node.js]: <http://nodejs.org>-   [Twitter Bootstrap]: <http://twitter.github.com/bootstrap/>-   [jQuery]: <http://jquery.com>-   [@tjholowaychuk]: <http://twitter.com/tjholowaychuk>-   [express]: <http://expressjs.com>-   [AngularJS]: <http://angularjs.org>-   [Gulp]: <http://gulpjs.com>--   [PlDb]: <https://github.com/joemccann/dillinger/tree/master/plugins/dropbox/README.md>-   [PlGh]: <https://github.com/joemccann/dillinger/tree/master/plugins/github/README.md>-   [PlGd]: <https://github.com/joemccann/dillinger/tree/master/plugins/googledrive/README.md>-   [PlOd]: <https://github.com/joemccann/dillinger/tree/master/plugins/onedrive/README.md>-   [PlMe]: <https://github.com/joemccann/dillinger/tree/master/plugins/medium/README.md>-   [PlGa]: <https://github.com/RahulHP/dillinger/blob/master/plugins/googleanalytics/README.md>
− testdata/lhs/input0.lhs.md
@@ -1,192 +0,0 @@-# Dillinger--[![N|Solid](https://cldup.com/dTxpPi9lDf.thumb.png)](https://nodesource.com/products/nsolid)--[![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master)](https://travis-ci.org/joemccann/dillinger)--Dillinger is a cloud-enabled, mobile-ready, offline-storage, AngularJS powered HTML5 Markdown editor.--- Type some Markdown on the left-- See HTML in the right-- Magic--## New Features--- Import a HTML file and watch it magically convert to Markdown-- Drag and drop images (requires your Dropbox account be linked)--You can also:--- Import and save files from GitHub, Dropbox, Google Drive and One Drive-- Drag and drop markdown and HTML files into Dillinger-- Export documents as Markdown, HTML and PDF--Markdown is a lightweight markup language based on the formatting conventions that people naturally use in email.  As [John Gruber] writes on the [Markdown site][df1]--> The overriding design goal for Markdown's-> formatting syntax is to make it as readable-> as possible. The idea is that a-> Markdown-formatted document should be-> publishable as-is, as plain text, without-> looking like it's been marked up with tags-> or formatting instructions.--This text you see here is *actually* written in Markdown! To get a feel for Markdown's syntax, type some text into the left window and watch the results in the right.--### Tech--Dillinger uses a number of open source projects to work properly:--- [AngularJS] - HTML enhanced for web apps!-- [Ace Editor] - awesome web-based text editor-- [markdown-it] - Markdown parser done right. Fast and easy to extend.-- [Twitter Bootstrap] - great UI boilerplate for modern web apps-- [node.js] - evented I/O for the backend-- [Express] - fast node.js network app framework [@tjholowaychuk]-- [Gulp] - the streaming build system-- [Breakdance](https://breakdance.github.io/breakdance/) - HTML to Markdown converter-- [jQuery] - duh--And of course Dillinger itself is open source with a [public repository][dill]- on GitHub.--### Installation--Dillinger requires [Node.js](https://nodejs.org/) v4+ to run.--Install the dependencies and devDependencies and start the server.--```console-cd dillinger-npm install -d-node app-```--For production environments...--```console-npm install --production-NODE_ENV=production node app-```--### Plugins--Dillinger is currently extended with the following plugins. Instructions on how to use them in your own application are linked below.--| Plugin | README |-| ------ | ------ |-| Dropbox | [plugins/dropbox/README.md][PlDb] |-| GitHub | [plugins/github/README.md][PlGh] |-| Google Drive | [plugins/googledrive/README.md][PlGd] |-| OneDrive | [plugins/onedrive/README.md][PlOd] |-| Medium | [plugins/medium/README.md][PlMe] |-| Google Analytics | [plugins/googleanalytics/README.md][PlGa] |--### Development--Want to contribute? Great!--Dillinger uses Gulp + Webpack for fast developing.-Make a change in your file and instantaneously see your updates!--Open your favorite Terminal and run these commands.--First Tab:--```console-node app-```--Second Tab:--```console-gulp watch-```--(optional) Third:--```console-karma test-```--#### Building for source--For production release:--```console-gulp build --prod-```--Generating pre-built zip archives for distribution:--```console-gulp build dist --prod-```--#### Docker--Dillinger is very easy to install and deploy in a Docker container.--By default, the Docker will expose port 8080, so change this within the Dockerfile if necessary. When ready, simply use the Dockerfile to build the image.--```console-cd dillinger-docker build -t joemccann/dillinger:${package.json.version} .-```--This will create the dillinger image and pull in the necessary dependencies. Be sure to swap out `${package.json.version}` with the actual version of Dillinger.--Once done, run the Docker image and map the port to whatever you wish on your host. In this example, we simply map port 8000 of the host to port 8080 of the Docker (or whatever port was exposed in the Dockerfile):--```console-docker run -d -p 8000:8080 --restart="always" <youruser>/dillinger:${package.json.version}-```--Verify the deployment by navigating to your server address in your preferred browser.--```console-127.0.0.1:8000-```--#### Kubernetes + Google Cloud--See [KUBERNETES.md](https://github.com/joemccann/dillinger/blob/master/KUBERNETES.md)--```haskell-module Main where- -main = putStrLn "HelloWorld"-```--### Todos--- Write MORE Tests-- Add Night Mode--### License--MIT--**Free Software, Hell Yeah!**--[//]: # (These are reference links used in the body of this note and get stripped out when the markdown processor does its job. There is no need to format nicely because it shouldn't be seen. Thanks SO - http://stackoverflow.com/questions/4823468/store-comments-in-markdown-syntax)--   [dill]: <https://github.com/joemccann/dillinger>-   [john gruber]: <http://daringfireball.net>-   [df1]: <http://daringfireball.net/projects/markdown/>-   [markdown-it]: <https://github.com/markdown-it/markdown-it>-   [Ace Editor]: <http://ace.ajax.org>-   [node.js]: <http://nodejs.org>-   [Twitter Bootstrap]: <http://twitter.github.com/bootstrap/>-   [jQuery]: <http://jquery.com>-   [@tjholowaychuk]: <http://twitter.com/tjholowaychuk>-   [express]: <http://expressjs.com>-   [AngularJS]: <http://angularjs.org>-   [Gulp]: <http://gulpjs.com>--   [PlDb]: <https://github.com/joemccann/dillinger/tree/master/plugins/dropbox/README.md>-   [PlGh]: <https://github.com/joemccann/dillinger/tree/master/plugins/github/README.md>-   [PlGd]: <https://github.com/joemccann/dillinger/tree/master/plugins/googledrive/README.md>-   [PlOd]: <https://github.com/joemccann/dillinger/tree/master/plugins/onedrive/README.md>-   [PlMe]: <https://github.com/joemccann/dillinger/tree/master/plugins/medium/README.md>-   [PlGa]: <https://github.com/RahulHP/dillinger/blob/master/plugins/googleanalytics/README.md>
− testdata/lhs/input0.lhs.md.lhs
@@ -1,170 +0,0 @@- # Dillinger--[![N|Solid](https://cldup.com/dTxpPi9lDf.thumb.png)](https://nodesource.com/products/nsolid)--[![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master)](https://travis-ci.org/joemccann/dillinger)--Dillinger is a cloud-enabled, mobile-ready, offline-storage, AngularJS powered HTML5 Markdown editor.--- Type some Markdown on the left-- See HTML in the right-- Magic-- ## New Features--- Import a HTML file and watch it magically convert to Markdown-- Drag and drop images (requires your Dropbox account be linked)--You can also:--- Import and save files from GitHub, Dropbox, Google Drive and One Drive-- Drag and drop markdown and HTML files into Dillinger-- Export documents as Markdown, HTML and PDF--Markdown is a lightweight markup language based on the formatting conventions that people naturally use in email.  As [John Gruber] writes on the [Markdown site][df1]-- > The overriding design goal for Markdown's- > formatting syntax is to make it as readable- > as possible. The idea is that a- > Markdown-formatted document should be- > publishable as-is, as plain text, without- > looking like it's been marked up with tags- > or formatting instructions.--This text you see here is *actually* written in Markdown! To get a feel for Markdown's syntax, type some text into the left window and watch the results in the right.-- ### Tech--Dillinger uses a number of open source projects to work properly:--- [AngularJS] - HTML enhanced for web apps!-- [Ace Editor] - awesome web-based text editor-- [markdown-it] - Markdown parser done right. Fast and easy to extend.-- [Twitter Bootstrap] - great UI boilerplate for modern web apps-- [node.js] - evented I/O for the backend-- [Express] - fast node.js network app framework [@tjholowaychuk]-- [Gulp] - the streaming build system-- [Breakdance](https://breakdance.github.io/breakdance/) - HTML to Markdown converter-- [jQuery] - duh--And of course Dillinger itself is open source with a [public repository][dill]- on GitHub.-- ### Installation--Dillinger requires [Node.js](https://nodejs.org/) v4+ to run.--Install the dependencies and devDependencies and start the server.--< cd dillinger-< npm install -d-< node app--For production environments...--< npm install --production-< NODE_ENV=production node app-- ### Plugins--Dillinger is currently extended with the following plugins. Instructions on how to use them in your own application are linked below.--| Plugin | README |-| ------ | ------ |-| Dropbox | [plugins/dropbox/README.md][PlDb] |-| GitHub | [plugins/github/README.md][PlGh] |-| Google Drive | [plugins/googledrive/README.md][PlGd] |-| OneDrive | [plugins/onedrive/README.md][PlOd] |-| Medium | [plugins/medium/README.md][PlMe] |-| Google Analytics | [plugins/googleanalytics/README.md][PlGa] |-- ### Development--Want to contribute? Great!--Dillinger uses Gulp + Webpack for fast developing.-Make a change in your file and instantaneously see your updates!--Open your favorite Terminal and run these commands.--First Tab:--< node app--Second Tab:--< gulp watch--(optional) Third:--< karma test-- #### Building for source--For production release:--< gulp build --prod--Generating pre-built zip archives for distribution:--< gulp build dist --prod-- #### Docker--Dillinger is very easy to install and deploy in a Docker container.--By default, the Docker will expose port 8080, so change this within the Dockerfile if necessary. When ready, simply use the Dockerfile to build the image.--< cd dillinger-< docker build -t joemccann/dillinger:${package.json.version} .--This will create the dillinger image and pull in the necessary dependencies. Be sure to swap out `${package.json.version}` with the actual version of Dillinger.--Once done, run the Docker image and map the port to whatever you wish on your host. In this example, we simply map port 8000 of the host to port 8080 of the Docker (or whatever port was exposed in the Dockerfile):--< docker run -d -p 8000:8080 --restart="always" <youruser>/dillinger:${package.json.version}--Verify the deployment by navigating to your server address in your preferred browser.--< 127.0.0.1:8000-- #### Kubernetes + Google Cloud--See [KUBERNETES.md](https://github.com/joemccann/dillinger/blob/master/KUBERNETES.md)--> module Main where->  -> main = putStrLn "HelloWorld"-- ### Todos--- Write MORE Tests-- Add Night Mode-- ### License--MIT--**Free Software, Hell Yeah!**--[//]: # (These are reference links used in the body of this note and get stripped out when the markdown processor does its job. There is no need to format nicely because it shouldn't be seen. Thanks SO - http://stackoverflow.com/questions/4823468/store-comments-in-markdown-syntax)--   [dill]: <https://github.com/joemccann/dillinger>-   [john gruber]: <http://daringfireball.net>-   [df1]: <http://daringfireball.net/projects/markdown/>-   [markdown-it]: <https://github.com/markdown-it/markdown-it>-   [Ace Editor]: <http://ace.ajax.org>-   [node.js]: <http://nodejs.org>-   [Twitter Bootstrap]: <http://twitter.github.com/bootstrap/>-   [jQuery]: <http://jquery.com>-   [@tjholowaychuk]: <http://twitter.com/tjholowaychuk>-   [express]: <http://expressjs.com>-   [AngularJS]: <http://angularjs.org>-   [Gulp]: <http://gulpjs.com>--   [PlDb]: <https://github.com/joemccann/dillinger/tree/master/plugins/dropbox/README.md>-   [PlGh]: <https://github.com/joemccann/dillinger/tree/master/plugins/github/README.md>-   [PlGd]: <https://github.com/joemccann/dillinger/tree/master/plugins/googledrive/README.md>-   [PlOd]: <https://github.com/joemccann/dillinger/tree/master/plugins/onedrive/README.md>-   [PlMe]: <https://github.com/joemccann/dillinger/tree/master/plugins/medium/README.md>-   [PlGa]: <https://github.com/RahulHP/dillinger/blob/master/plugins/googleanalytics/README.md>
− testdata/lhs/input1.lhs
@@ -1,233 +0,0 @@- # Monoid reduction parsing/execution--I gave our apprentice the excersise to write a program-that takes strings in the form--< "1+4+2+3"--with variable length and write it in way that it is easily-extendible to strings of the form--< "1*2*3*4"--Not a hugely interesting task normally.-However, after giving the task, it occurred to me, that-the examples involved evaluating Monoids, specifically-the product and sum monoid on natural numbers.--This got me thinking:-What would be an elegant, extendible way to write such a-program?--This is what I came up with:--I am using these language extensions, I will explain their usage-when it comes up.--> {-# LANGUAGE MultiParamTypeClasses #-}-> {-# LANGUAGE FlexibleContexts #-}-> {-# LANGUAGE FlexibleInstances #-}-> {-# LANGUAGE InstanceSigs #-}--Next, let's import the needed modules:--> module Main where-> import Data.Monoid ( Product(..), Sum(..))          -- obviously when we are talking monoids -> import Data.List ( intercalate)            -- an interesting monoid, and some helper functions-- ## What we want to have--What is is exactly what we want to have? We want something-that expresses an idea for reducing a monoid string (a string-that expresses a monoid computation) that generalizes over-any monoid we throw at it.--In Haskell we use a typeclass for that:--> class (Monoid (m a)) => Reducible m a where->         op     :: m a -> Char->         constr :: m a -> a -> m a->         get    :: m a -> m a -> a--This states that a `Reducible` has to be a `Monoid`-and we would like to konw what `Char` `op`erand it is associated with.-Additioally, we would like to know how to construct the `Monoid` form-a value and a way to extract a value from the `Monoid`.-Here `MultiParamTypeClasses` enables us to writd `Reducible m a`-so that monoids with different base types can behave differently.--Now that we have written down what we want, we can try to implement-a `Reducible`. To begin, we will start with the `Sum` and `Product`-monoids mentioned in the beginning:--> instance (Num a) => Reducible Sum a where->         op :: Num a => Sum a -> Char->         op     _ = '+'->         constr :: Num a => Sum a -> a -> Sum a->         constr _ = Sum->         get :: Num a => Sum a -> Sum a -> a->         get    _ = getSum-> -> instance (Num a) => Reducible Product a where->         op :: Num a => Product a -> Char->         op     _ = '*'->         constr :: Num a => Product a -> a -> Product a->         constr _ = Product->         get :: Num a => Product a -> Product a -> a->         get    _ = getProduct--As you might have noticed, we completely ignore the given argument in all-three functions. The argument is merely used as a phantom type, for-the compiler to disambiguate what instance (and with it, which dictionary)-will be used at runtime.-The `FlexibleInstances` extension allows us to specify an instance with a-`Num` constraint, to let us use the two instances with all `Num` instances.-- ## How do we work with this?--Let's define how we actually reduce a "monoid string" to the value it-represents.--The function `reduce'` takes a string and the information what monoid-the string represents and calculates the value from it:--> reduce' :: (Read a, Reducible m a) => String -> m a -> a --We need the Read a to make sure, that we can actually parse the values-the monoid holds. It would be nicer and safer to use a proper parser here-but for the sake of simplicity we keep it like that.-(We can always wrap a `pureTry` around the call to `reduce'`, which, again,-is not a nice complete way to do it, but easier. ^^)-Now let's drop reduce's curtain of uncertainty:--> reduce' list red = get red  ->                 $ foldr (<>) mempty->                 $ map (\x -> constr red $ read x) ->                 $ splitOn (op red) list --What is going on here?-It's actually quite simple:-First, we split the string (`list`) by the reducibles operator.-Then we convert each value in the resulting list to the monoid itself.-This leaves us with a list of monoid values, that we can reduce with a-fold and the monoid operation `<>` and finally we `get` the value-out of the monoid, using the reducibles info on how to do that.--The `splitOn` function itself is rather boilerplate:--> splitOn :: (Eq a) => a -> [a] -> [[a]]-> splitOn _ [] = [[]]-> splitOn c (h:t) ->   | h == c    = []:rest->   | otherwise = (h:head rest):tail rest->    where rest = splitOn c t --no funny business here.-- ## However--... we would like an interface that is a bit simpler.-Wouldn't it be nice, to write--< reduce "1+2+3+4"--and get back the `Int` it computes to?--Let's write such a function:--> reduce :: String -> Int--The type now restricts us to returning `Int`s, but we could easily write-another function that returns strings, etc.-(We could even use TemplateHaskell or something along the `Typeable` and-`Data` typeclasses to automatically write such functions or return polymorphic-results and work with them, but that is something for another post.)--> reduce s->     | '+' `elem` s = reduce' s (mempty :: Sum Int)->     | '*' `elem` s = reduce' s (mempty :: Product Int)->     | ':' `elem` s = reduce' s (mempty :: [Int])->     | otherwise    = error "no calculatable operation detected"--The implementation is actually rather straight forward:-we just peek into the string what operand we find and then make a call to-`reduce'` with the proper monoid type.-Probably you have found the `'.'` by now and wonder if you missed something-up until now.-Don't fear, you haven't.-I have saved somthing interesting for the end.-You probably konw that Lists also have a monoid instance in Haskell.-Therefore, we can reasonably write an instance for `Reducible`saved somthing interesting for the end.-You probably konw that Lists also have a monoid instance in Haskell.-Therefore, we can reasonably write an instance for `Reducible`:--> instance Reducible [] Int where->   op :: [Int] -> Char->   op     _ = ':'->   constr :: [Int] -> Int -> [Int]->   constr _ = (:[])->   get :: [Int] -> [Int] -> Int->   get    _ = sum -- length -- basically all [a] -> Int Functions--which even generalizes the behavior from above.-Note that the reduction part has now moved to the `get` function instead of-the binary monoid operation `<>`.-What is this good for you ask?-Monoids are bound by certain laws<sup id="laws">[1](#fn_laws)</sup>.--< -- Identity laws-< x <> mempty = x-< mempty <> x = x-< -< -- Associativity-< (x <> y) <> z = x <> (y <> z)--These state that the binary operation needs to be associative (so we couldn't have-a monoid operation for division for example, as this would violate associativity.-However, moving the reduction operation to the reducible instead, we can specify-a function that is not associative by just implementing a function from--< [Int] -> Int--For example:--< foldl (/) 1 list--or even have a non binary function such as--< f l = func l True-<   where-<     func []       _     = []-<     func (h:[])   _     = h-<     func (x:y:ls) True  = x*y + (func ls False)-<     func (x:y:ls) False = x/y - (func ls True)--which is sensitive to where in the list the operands are.--As an additional example this is how one could implement string concattenation-using a reducible:--> instance Reducible [] String where->   op :: [String] -> Char->   op     _ = '.'->   constr :: [String] -> String -> [String]->   constr _ = (:[])->   get :: [String] -> [String] -> String->   get    _ = intercalate [] -- ## Final Words--The final example along with the ability to shift the reduction step to the-reducible opens up insersting applications for implementing simple DSL computations-or just fast, easily understandable parsers for complex data structures.-(Like implementing a `Reducible [] Foo` where Foo is some complex data type.)--> main :: IO ()-> main = do->         print $ reduce "1+2+3+4"->         print $ reduce "2*3*4"->         print $ reduce "2:3:4" ->         print $ reduce' "\"hello \".\"world\"" (mempty :: [String])------------------------------------------------------------------------------------------<b name="fn_laws">1</b>: These are lazily taken from the [Haskell Website](https://wiki.haskell.org/Monoid)[<-](#laws)
− testdata/lhs/input1.lhs.md
@@ -1,271 +0,0 @@-# Monoid reduction parsing/execution--I gave our apprentice the excersise to write a program-that takes strings in the form--```console-"1+4+2+3"-```--with variable length and write it in way that it is easily-extendible to strings of the form--```console-"1*2*3*4"-```--Not a hugely interesting task normally.-However, after giving the task, it occurred to me, that-the examples involved evaluating Monoids, specifically-the product and sum monoid on natural numbers.--This got me thinking:-What would be an elegant, extendible way to write such a-program?--This is what I came up with:--I am using these language extensions, I will explain their usage-when it comes up.--```haskell-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE InstanceSigs #-}-```--Next, let's import the needed modules:--```haskell-module Main where-import Data.Monoid ( Product(..), Sum(..))          -- obviously when we are talking monoids -import Data.List ( intercalate)            -- an interesting monoid, and some helper functions-```--## What we want to have--What is is exactly what we want to have? We want something-that expresses an idea for reducing a monoid string (a string-that expresses a monoid computation) that generalizes over-any monoid we throw at it.--In Haskell we use a typeclass for that:--```haskell-class (Monoid (m a)) => Reducible m a where-        op     :: m a -> Char-        constr :: m a -> a -> m a-        get    :: m a -> m a -> a-```--This states that a `Reducible` has to be a `Monoid`-and we would like to konw what `Char` `op`erand it is associated with.-Additioally, we would like to know how to construct the `Monoid` form-a value and a way to extract a value from the `Monoid`.-Here `MultiParamTypeClasses` enables us to writd `Reducible m a`-so that monoids with different base types can behave differently.--Now that we have written down what we want, we can try to implement-a `Reducible`. To begin, we will start with the `Sum` and `Product`-monoids mentioned in the beginning:--```haskell-instance (Num a) => Reducible Sum a where-        op :: Num a => Sum a -> Char-        op     _ = '+'-        constr :: Num a => Sum a -> a -> Sum a-        constr _ = Sum-        get :: Num a => Sum a -> Sum a -> a-        get    _ = getSum--instance (Num a) => Reducible Product a where-        op :: Num a => Product a -> Char-        op     _ = '*'-        constr :: Num a => Product a -> a -> Product a-        constr _ = Product-        get :: Num a => Product a -> Product a -> a-        get    _ = getProduct-```--As you might have noticed, we completely ignore the given argument in all-three functions. The argument is merely used as a phantom type, for-the compiler to disambiguate what instance (and with it, which dictionary)-will be used at runtime.-The `FlexibleInstances` extension allows us to specify an instance with a-`Num` constraint, to let us use the two instances with all `Num` instances.--## How do we work with this?--Let's define how we actually reduce a "monoid string" to the value it-represents.--The function `reduce'` takes a string and the information what monoid-the string represents and calculates the value from it:--```haskell-reduce' :: (Read a, Reducible m a) => String -> m a -> a -```--We need the Read a to make sure, that we can actually parse the values-the monoid holds. It would be nicer and safer to use a proper parser here-but for the sake of simplicity we keep it like that.-(We can always wrap a `pureTry` around the call to `reduce'`, which, again,-is not a nice complete way to do it, but easier. ^^)-Now let's drop reduce's curtain of uncertainty:--```haskell-reduce' list red = get red  -                $ foldr (<>) mempty-                $ map (\x -> constr red $ read x) -                $ splitOn (op red) list -```--What is going on here?-It's actually quite simple:-First, we split the string (`list`) by the reducibles operator.-Then we convert each value in the resulting list to the monoid itself.-This leaves us with a list of monoid values, that we can reduce with a-fold and the monoid operation `<>` and finally we `get` the value-out of the monoid, using the reducibles info on how to do that.--The `splitOn` function itself is rather boilerplate:--```haskell-splitOn :: (Eq a) => a -> [a] -> [[a]]-splitOn _ [] = [[]]-splitOn c (h:t) -  | h == c    = []:rest-  | otherwise = (h:head rest):tail rest-   where rest = splitOn c t -```--no funny business here.--## However--... we would like an interface that is a bit simpler.-Wouldn't it be nice, to write--```console-reduce "1+2+3+4"-```--and get back the `Int` it computes to?--Let's write such a function:--```haskell-reduce :: String -> Int-```--The type now restricts us to returning `Int`s, but we could easily write-another function that returns strings, etc.-(We could even use TemplateHaskell or something along the `Typeable` and-`Data` typeclasses to automatically write such functions or return polymorphic-results and work with them, but that is something for another post.)--```haskell-reduce s-    | '+' `elem` s = reduce' s (mempty :: Sum Int)-    | '*' `elem` s = reduce' s (mempty :: Product Int)-    | ':' `elem` s = reduce' s (mempty :: [Int])-    | otherwise    = error "no calculatable operation detected"-```--The implementation is actually rather straight forward:-we just peek into the string what operand we find and then make a call to-`reduce'` with the proper monoid type.-Probably you have found the `'.'` by now and wonder if you missed something-up until now.-Don't fear, you haven't.-I have saved somthing interesting for the end.-You probably konw that Lists also have a monoid instance in Haskell.-Therefore, we can reasonably write an instance for `Reducible`saved somthing interesting for the end.-You probably konw that Lists also have a monoid instance in Haskell.-Therefore, we can reasonably write an instance for `Reducible`:--```haskell-instance Reducible [] Int where-  op :: [Int] -> Char-  op     _ = ':'-  constr :: [Int] -> Int -> [Int]-  constr _ = (:[])-  get :: [Int] -> [Int] -> Int-  get    _ = sum -- length -- basically all [a] -> Int Functions-```--which even generalizes the behavior from above.-Note that the reduction part has now moved to the `get` function instead of-the binary monoid operation `<>`.-What is this good for you ask?-Monoids are bound by certain laws<sup id="laws">[1](#fn_laws)</sup>.--```console--- Identity laws-x <> mempty = x-mempty <> x = x---- Associativity-(x <> y) <> z = x <> (y <> z)-```--These state that the binary operation needs to be associative (so we couldn't have-a monoid operation for division for example, as this would violate associativity.-However, moving the reduction operation to the reducible instead, we can specify-a function that is not associative by just implementing a function from--```console-[Int] -> Int-```--For example:--```console-foldl (/) 1 list-```--or even have a non binary function such as--```console-f l = func l True-  where-    func []       _     = []-    func (h:[])   _     = h-    func (x:y:ls) True  = x*y + (func ls False)-    func (x:y:ls) False = x/y - (func ls True)-```--which is sensitive to where in the list the operands are.--As an additional example this is how one could implement string concattenation-using a reducible:--```haskell-instance Reducible [] String where-  op :: [String] -> Char-  op     _ = '.'-  constr :: [String] -> String -> [String]-  constr _ = (:[])-  get :: [String] -> [String] -> String-  get    _ = intercalate [] -```--## Final Words--The final example along with the ability to shift the reduction step to the-reducible opens up insersting applications for implementing simple DSL computations-or just fast, easily understandable parsers for complex data structures.-(Like implementing a `Reducible [] Foo` where Foo is some complex data type.)--```haskell-main :: IO ()-main = do-        print $ reduce "1+2+3+4"-        print $ reduce "2*3*4"-        print $ reduce "2:3:4" -        print $ reduce' "\"hello \".\"world\"" (mempty :: [String])-```------------------------------------------------------------------------------------------<b name="fn_laws">1</b>: These are lazily taken from the [Haskell Website](https://wiki.haskell.org/Monoid)[<-](#laws)
− testdata/lhs/input1.lhs.md.lhs
@@ -1,233 +0,0 @@- # Monoid reduction parsing/execution--I gave our apprentice the excersise to write a program-that takes strings in the form--< "1+4+2+3"--with variable length and write it in way that it is easily-extendible to strings of the form--< "1*2*3*4"--Not a hugely interesting task normally.-However, after giving the task, it occurred to me, that-the examples involved evaluating Monoids, specifically-the product and sum monoid on natural numbers.--This got me thinking:-What would be an elegant, extendible way to write such a-program?--This is what I came up with:--I am using these language extensions, I will explain their usage-when it comes up.--> {-# LANGUAGE MultiParamTypeClasses #-}-> {-# LANGUAGE FlexibleContexts #-}-> {-# LANGUAGE FlexibleInstances #-}-> {-# LANGUAGE InstanceSigs #-}--Next, let's import the needed modules:--> module Main where-> import Data.Monoid ( Product(..), Sum(..))          -- obviously when we are talking monoids -> import Data.List ( intercalate)            -- an interesting monoid, and some helper functions-- ## What we want to have--What is is exactly what we want to have? We want something-that expresses an idea for reducing a monoid string (a string-that expresses a monoid computation) that generalizes over-any monoid we throw at it.--In Haskell we use a typeclass for that:--> class (Monoid (m a)) => Reducible m a where->         op     :: m a -> Char->         constr :: m a -> a -> m a->         get    :: m a -> m a -> a--This states that a `Reducible` has to be a `Monoid`-and we would like to konw what `Char` `op`erand it is associated with.-Additioally, we would like to know how to construct the `Monoid` form-a value and a way to extract a value from the `Monoid`.-Here `MultiParamTypeClasses` enables us to writd `Reducible m a`-so that monoids with different base types can behave differently.--Now that we have written down what we want, we can try to implement-a `Reducible`. To begin, we will start with the `Sum` and `Product`-monoids mentioned in the beginning:--> instance (Num a) => Reducible Sum a where->         op :: Num a => Sum a -> Char->         op     _ = '+'->         constr :: Num a => Sum a -> a -> Sum a->         constr _ = Sum->         get :: Num a => Sum a -> Sum a -> a->         get    _ = getSum-> -> instance (Num a) => Reducible Product a where->         op :: Num a => Product a -> Char->         op     _ = '*'->         constr :: Num a => Product a -> a -> Product a->         constr _ = Product->         get :: Num a => Product a -> Product a -> a->         get    _ = getProduct--As you might have noticed, we completely ignore the given argument in all-three functions. The argument is merely used as a phantom type, for-the compiler to disambiguate what instance (and with it, which dictionary)-will be used at runtime.-The `FlexibleInstances` extension allows us to specify an instance with a-`Num` constraint, to let us use the two instances with all `Num` instances.-- ## How do we work with this?--Let's define how we actually reduce a "monoid string" to the value it-represents.--The function `reduce'` takes a string and the information what monoid-the string represents and calculates the value from it:--> reduce' :: (Read a, Reducible m a) => String -> m a -> a --We need the Read a to make sure, that we can actually parse the values-the monoid holds. It would be nicer and safer to use a proper parser here-but for the sake of simplicity we keep it like that.-(We can always wrap a `pureTry` around the call to `reduce'`, which, again,-is not a nice complete way to do it, but easier. ^^)-Now let's drop reduce's curtain of uncertainty:--> reduce' list red = get red  ->                 $ foldr (<>) mempty->                 $ map (\x -> constr red $ read x) ->                 $ splitOn (op red) list --What is going on here?-It's actually quite simple:-First, we split the string (`list`) by the reducibles operator.-Then we convert each value in the resulting list to the monoid itself.-This leaves us with a list of monoid values, that we can reduce with a-fold and the monoid operation `<>` and finally we `get` the value-out of the monoid, using the reducibles info on how to do that.--The `splitOn` function itself is rather boilerplate:--> splitOn :: (Eq a) => a -> [a] -> [[a]]-> splitOn _ [] = [[]]-> splitOn c (h:t) ->   | h == c    = []:rest->   | otherwise = (h:head rest):tail rest->    where rest = splitOn c t --no funny business here.-- ## However--... we would like an interface that is a bit simpler.-Wouldn't it be nice, to write--< reduce "1+2+3+4"--and get back the `Int` it computes to?--Let's write such a function:--> reduce :: String -> Int--The type now restricts us to returning `Int`s, but we could easily write-another function that returns strings, etc.-(We could even use TemplateHaskell or something along the `Typeable` and-`Data` typeclasses to automatically write such functions or return polymorphic-results and work with them, but that is something for another post.)--> reduce s->     | '+' `elem` s = reduce' s (mempty :: Sum Int)->     | '*' `elem` s = reduce' s (mempty :: Product Int)->     | ':' `elem` s = reduce' s (mempty :: [Int])->     | otherwise    = error "no calculatable operation detected"--The implementation is actually rather straight forward:-we just peek into the string what operand we find and then make a call to-`reduce'` with the proper monoid type.-Probably you have found the `'.'` by now and wonder if you missed something-up until now.-Don't fear, you haven't.-I have saved somthing interesting for the end.-You probably konw that Lists also have a monoid instance in Haskell.-Therefore, we can reasonably write an instance for `Reducible`saved somthing interesting for the end.-You probably konw that Lists also have a monoid instance in Haskell.-Therefore, we can reasonably write an instance for `Reducible`:--> instance Reducible [] Int where->   op :: [Int] -> Char->   op     _ = ':'->   constr :: [Int] -> Int -> [Int]->   constr _ = (:[])->   get :: [Int] -> [Int] -> Int->   get    _ = sum -- length -- basically all [a] -> Int Functions--which even generalizes the behavior from above.-Note that the reduction part has now moved to the `get` function instead of-the binary monoid operation `<>`.-What is this good for you ask?-Monoids are bound by certain laws<sup id="laws">[1](#fn_laws)</sup>.--< -- Identity laws-< x <> mempty = x-< mempty <> x = x-< -< -- Associativity-< (x <> y) <> z = x <> (y <> z)--These state that the binary operation needs to be associative (so we couldn't have-a monoid operation for division for example, as this would violate associativity.-However, moving the reduction operation to the reducible instead, we can specify-a function that is not associative by just implementing a function from--< [Int] -> Int--For example:--< foldl (/) 1 list--or even have a non binary function such as--< f l = func l True-<   where-<     func []       _     = []-<     func (h:[])   _     = h-<     func (x:y:ls) True  = x*y + (func ls False)-<     func (x:y:ls) False = x/y - (func ls True)--which is sensitive to where in the list the operands are.--As an additional example this is how one could implement string concattenation-using a reducible:--> instance Reducible [] String where->   op :: [String] -> Char->   op     _ = '.'->   constr :: [String] -> String -> [String]->   constr _ = (:[])->   get :: [String] -> [String] -> String->   get    _ = intercalate [] -- ## Final Words--The final example along with the ability to shift the reduction step to the-reducible opens up insersting applications for implementing simple DSL computations-or just fast, easily understandable parsers for complex data structures.-(Like implementing a `Reducible [] Foo` where Foo is some complex data type.)--> main :: IO ()-> main = do->         print $ reduce "1+2+3+4"->         print $ reduce "2*3*4"->         print $ reduce "2:3:4" ->         print $ reduce' "\"hello \".\"world\"" (mempty :: [String])------------------------------------------------------------------------------------------<b name="fn_laws">1</b>: These are lazily taken from the [Haskell Website](https://wiki.haskell.org/Monoid)[<-](#laws)
+ testdata/md/test.hs view
@@ -0,0 +1,35 @@+{- LIMA_INDENT 3 -}++{- LIMA_DISABLE -}++-- What's the answer?++{- LIMA_ENABLE -}++{- LIMA_INDENT 1 -}++{- LIMA_INDENT 2 -}++{-+- Intermediate results+-}++a = const 3+b = a 4++{- LIMA_DEDENT -}++answer = b * 14++{- Hello from comments,++world!+-}++{-+Hello from text,+world!++And from+here!+-}
+ testdata/md/test.lhs view
@@ -0,0 +1,29 @@+% LIMA_INDENT 3++% LIMA_DISABLE++% -- What's the answer?++% LIMA_ENABLE++% LIMA_INDENT 1++% LIMA_INDENT 2++- Intermediate results+>   a = const 3+>   b = a 4++% LIMA_DEDENT++> answer = b * 14++% Hello from comments,++% world!++Hello from text,+world!++And from+here!
+ testdata/md/test.md view
@@ -0,0 +1,35 @@+   <!-- LIMA_INDENT 3 -->++<!-- LIMA_DISABLE++-- What's the answer?++LIMA_ENABLE -->++ <!-- LIMA_INDENT 1 -->++  <!-- LIMA_INDENT 2 -->++- Intermediate results++  ```haskell+  a = const 3+  b = a 4+  ```++<!-- LIMA_DEDENT -->++```haskell+answer = b * 14+```++<!-- Hello from comments,++world!+-->++Hello from text,+world!++And from+here!
+ testdata/md/test.tex view
@@ -0,0 +1,33 @@+% LIMA_INDENT 3++   a =+     f b++% LIMA_DEDENT++a =+  f b++% LIMA_INDENT 2++% LIMA_INDENT 5++     a =+       f b++% Hello,++% world!++Line 1+Line 2++Line 1+Line 2++% LIMA_DISABLE++% Line 1+% Line 2++% LIMA_ENABLE
+ testdata/md/tokens.hs view
@@ -0,0 +1,34 @@+[ Indent+    { n = 3 }+, Disabled+    { manyLines = [ "-- What's the answer?" ] }+, Indent+    { n = 1 }+, Indent+    { n = 2 }+, Text+    { someLines = "- Intermediate results" :| [] }+, HaskellCode+    { manyLines =+        [ "b = a 4"+        , "a = const 3"+        ]+    }+, Dedent+, HaskellCode+    { manyLines = [ "answer = b * 14" ] }+, Comment+    { someLines = "world!" :|+        [ ""+        , "Hello from comments,"+        ]+    }+, Text+    { someLines = "here!" :|+        [ "And from"+        , ""+        , "world!"+        , "Hello from text,"+        ]+    }+]
+ testdata/tex/test.hs view
@@ -0,0 +1,44 @@+{- LIMA_DISABLE -}++-- What's the answer?++{- LIMA_ENABLE -}++{- LIMA_INDENT 1 -}++{- LIMA_INDENT 2 -}++{-+Intermediate results++\begin{code}+-}++a = const 3+b = a 4++{-+\end{code}+-}++{- LIMA_DEDENT -}++{-+\begin{code}+-}++answer = b * 14++{-+\end{code}+-}++{- Hello from comments,++world!+-}++{-+Hello from text,+world!+-}
+ testdata/tex/test.lhs view
@@ -0,0 +1,29 @@+% LIMA_DISABLE++% -- What's the answer?++% LIMA_ENABLE++% LIMA_INDENT 1++% LIMA_INDENT 2++Intermediate results++\begin{code}+>   a = const 3+>   b = a 4+\end{code}++% LIMA_DEDENT++\begin{code}+> answer = b * 14+\end{code}++% Hello from comments,++% world!++Hello from text,+world!
+ testdata/tex/test.md view
@@ -0,0 +1,38 @@+<!-- LIMA_DISABLE++-- What's the answer?++LIMA_ENABLE -->++ <!-- LIMA_INDENT 1 -->++  <!-- LIMA_INDENT 2 -->++Intermediate results++\begin{code}++  ```haskell+  a = const 3+  b = a 4+  ```++\end{code}++<!-- LIMA_DEDENT -->++\begin{code}++```haskell+answer = b * 14+```++\end{code}++<!-- Hello from comments,++world!+-->++Hello from text,+world!
+ testdata/tex/test.tex view
@@ -0,0 +1,29 @@+% LIMA_DISABLE++% -- What's the answer?++% LIMA_ENABLE++% LIMA_INDENT 1++% LIMA_INDENT 2++Intermediate results++\begin{code}+  a = const 3+  b = a 4+\end{code}++% LIMA_DEDENT++\begin{code}+answer = b * 14+\end{code}++% Hello from comments,++% world!++Hello from text,+world!
+ testdata/tex/tokens.hs view
@@ -0,0 +1,36 @@+[ Disabled+    { manyLines = [ "-- What's the answer?" ] }+, Indent+    { n = 1 }+, Indent+    { n = 2 }+, Text+    { someLines = "\\begin{code}" :|+        [ ""+        , "Intermediate results"+        ]+    }+, HaskellCode+    { manyLines =+        [ "b = a 4"+        , "a = const 3"+        ]+    }+, Text+    { someLines = "\\end{code}" :| [] }+, Dedent+, Text+    { someLines = "\\begin{code}" :| [] }+, HaskellCode+    { manyLines = [ "answer = b * 14" ] }+, Text+    { someLines = "\\end{code}" :| [] }+, Comment+    { someLines = "world!" :|+        [ ""+        , "Hello from comments,"+        ]+    }+, Text+    { someLines = "world!" :| [ "Hello from text," ] }+]