lima 0.1.0.1 → 0.1.0.2
raw patch · 4 files changed
+110/−68 lines, 4 files
Files
- README.md +13/−9
- app/Main.hs +5/−6
- lima.cabal +1/−1
- src/Converter.hs +91/−52
README.md view
@@ -19,13 +19,15 @@ [Examples](./testdata/hs) -Rules for `.hs` -> `.md` conversion:+Rules: - To produce text blocks, write multiline comments in `GitHub`-flavoured `Markdown` - Such comments should start with `{- ` or `{-\n` - Multiline comments (even `{- -}`) split `Haskell` code into snippets - Special comments like `{- FOURMOLU_ENABLE -}` won't appear in a `.md`. You can supply other comments in a config (`hs-md.special-comments`). See the sample [config](./testdata/config/). - You can ignore parts of a `.hs` file by enclosing them into `{- LIMA_DISABLE -}` and `{- LIMA_ENABLE -}`. The lines between such comments will be commented out in the resulting `.md`.+- You can indent a Haskell snippet by writing a magic comment `{- LIMA_INDENT N -}`, where `N` is an integer denoting the new indentation level. Reset the indentation by writing `{- LIMA_DEDENT -}`.+ - If your snippet relates to some text, put the magic comment before that text ### lhs <-> md @@ -34,17 +36,19 @@ - [input0.lhs](./testdata/input0.lhs) -> [input0.lhs.md](./testdata/input0.lhs.md) - [input1.lhs](./testdata/input1.lhs) -> [input1.lhs.md](./testdata/input1.lhs.md) -As `.lhs` doesn't support `#` (heading) or `>` (quotation start) at a line beginning, one should write ` #` and ` >` instead.+Rules: -- ` #` -> `#` -> ` #`-- ` >` -> `>` -> ` >`+- As `.lhs` doesn't support `#` (heading) or `>` (quotation start) at a line beginning, one should write ` #` and ` >` instead. -If you'd like to provide some code in a `.lhs`, follow these rules:+ - ` #` -> `#` -> ` #`+ - ` >` -> `>` -> ` >` -- `>` is for `Haskell` code, there should be an empty line before and after the block with `Haskell` code-- `<` is for any other code. Such code will be converted into code blocks of type `console` in `.md`-- The round-trip property is not guarranteed if you insert code snippets into `.lhs` using three backticks- - Nevertheless, feel free to insert them into `.md`. In `.lhs`, they will just lose the language info+- If you'd like to provide some code snippet in a `.lhs`, follow these rules:++ - `>` is for `Haskell` code, there should be an empty line before and after the block with `Haskell` code+ - `<` is for any other code. Such code will be converted into code blocks of type `console` in `.md`+ - The round-trip property is not guarranteed if you insert code snippets into `.lhs` using three backticks+ - Nevertheless, feel free to insert them into `.md`. In `.lhs`, they will just lose the language info ## Command-line tool
app/Main.hs view
@@ -86,21 +86,20 @@ header_ :: Doc header_ = descriptionBlock- [ bold "lima" <-> "converts between" <-> bold "Haskell" <> lparen <> bold ".hs" <> rparen- , "and" <-> bold "Markdown" <> lparen <> bold ".md" <> rparen- , "and between" <-> bold "Literate Haskell" <> lparen <> bold ".lhs" <> rparen- , "and" <-> bold "Markdown" <> lparen <> bold ".md" <> rparen <> dot+ [ 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 "Markdown" <+> "using the config" <+> bold "config.yaml" <> colon <> 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 "Markdown" <> colon <> fill'+ , indent 2 $ "Convert" <+> bold "LHS" <+> "to" <+> bold "MD" <> colon <> fill' , indent 2 $ "file1.lhs -> file1.lhs.md" <> comma <-> "file2.lhs -> file2.lhs.md" ]
lima.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: lima-version: 0.1.0.1+version: 0.1.0.2 synopsis: (Haskell or Literate Haskell) <-> Markdown converter description: See the [README.md](https://github.com/deemp/flakes/tree/main/lima#readme) category: Productivity
src/Converter.hs view
@@ -3,14 +3,16 @@ {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}-{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}- {-# 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, (.:), (.:?))@@ -31,6 +33,9 @@ "CommentsToIgnore" (\v -> ConfigHs2Md <$> v .: "special-comments") +backticks_ :: Int -> String+backticks_ n = indentN n backticks+ backticks :: String backticks = "```" @@ -140,11 +145,17 @@ _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"]+specialCommentsDefault = ["FOURMOLU_DISABLE", "FOURMOLU_ENABLE", _LIMA_INDENT, _LIMA_DEDENT] endsWith :: String -> String -> Bool endsWith = flip isSuffixOf@@ -156,6 +167,7 @@ mcOpenSpace :: String mcOpenSpace = mcOpen ++ " " +-- | multiline comment closing symbols mcClose :: String mcClose = "-}" @@ -168,8 +180,11 @@ backticksHs :: String backticksHs = backticks ++ "haskell" +backticksHs_ :: Int -> String+backticksHs_ n = indentN n backticksHs+ dropEmpties :: [String] -> [String]-dropEmpties = dropWhile (== "")+dropEmpties = dropWhile (\x -> dropSpaces x == "") -- Markdown comments @@ -187,65 +202,83 @@ mdcCloseSpace :: String mdcCloseSpace = " " ++ mdcClose -stripMc :: String -> String-stripMc x = dropEnd (length mcCloseSpace) (drop (length mcOpenSpace) x)+-- | 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 x []) . lines+hsToMd ConfigHs2Md{..} = unlines . dropWhile (== "") . reverse . (\x -> convert True False False 0 x []) . lines where specialComments_ = specialCommentsDefault ++ specialComments- convert :: Bool -> Bool -> Bool -> [String] -> [String] -> [String]- convert inLimaEnable inComments inSnippet (h : hs) res+ convert :: Bool -> Bool -> Bool -> Int -> [String] -> [String] -> [String]+ convert inLimaEnable inComments inSnippet indent_ (h : hs) res | not inComments = if | -- disable -- split a snippet- h `startsWith` (mcOpenSpace ++ _LIMA_DISABLE) ->- (convert False False False hs)- ([mdcOpenSpace ++ _LIMA_DISABLE] ++ [backticks | inSnippet] ++ res)+ h == mcOpenSpace ++ _LIMA_DISABLE ++ mcCloseSpace ->+ (convert False False False indent_ hs)+ ([mdcOpenSpace ++ _LIMA_DISABLE] ++ [backticks_ indent_ | inSnippet] ++ dropEmpties res) | -- enable- h `startsWith` (mcOpenSpace ++ _LIMA_ENABLE) ->- convert True False inSnippet hs ((_LIMA_ENABLE ++ mdcCloseSpace) : res)- | -- if disabled+ h == mcOpenSpace ++ _LIMA_ENABLE ++ mcCloseSpace ->+ convert True False inSnippet indent_ hs ((_LIMA_ENABLE ++ mdcCloseSpace) : res)+ | -- disabled not inLimaEnable ->- convert inLimaEnable False False hs (h : res)+ 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+ in (convert inLimaEnable False False newIndent_ hs)+ ( [mdcOpenSpace ++ stripMC h ++ mdcCloseSpace]+ ++ ["" | inSnippet]+ ++ [backticks_ indent_ | inSnippet]+ ++ (if isIndent then squashEmpties else dropEmpties) res+ ) | -- a special comment- -- comment should be in multi-line style like '{- FOURMOLU_DISABLE -}'+ -- 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 hs)- ([mdcOpenSpace ++ stripMc h ++ mdcCloseSpace] ++ ["" | inSnippet] ++ [backticks | inSnippet] ++ dropEmpties res)+ (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 | inSnippet]+ 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 hs ([dropEnd (length mcCloseSpace) x'] ++ pref ++ res')- else convert inLimaEnable True False hs ([x' | not (null x')] ++ pref ++ res')+ 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 hs ([h, backticksHs] ++ squashEmpties res)+ 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 hs res+ convert inLimaEnable False False indent_ hs res | inSnippet ->- convert inLimaEnable False True hs (h : res)+ convert inLimaEnable False True indent_ hs ((indentN indent_ h) : res) | inComments =- if -- end of a multiline comment- h `startsWith` mcClose- then convert inLimaEnable False False hs res+ 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 hs (h : res)- convert _ _ inSnippet [] res =- [backticks | inSnippet] ++ dropEmpties res+ 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) x)@@ -253,51 +286,57 @@ 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 x []) . lines+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 -> [String] -> [String] -> [String]- convert inText inSnippet inComments (h : hs) res+ convert :: Bool -> Bool -> Bool -> Int -> [String] -> [String] -> [String]+ convert inText inSnippet inComments dedent_ (h : hs) res | inComments =- if -- enable- h `startsWith` (_LIMA_ENABLE ++ mdcCloseSpace)+ -- enable+ if h `startsWith` (_LIMA_ENABLE ++ mdcCloseSpace) then -- split text - (convert inText inSnippet False hs)+ (convert inText inSnippet False dedent_ hs) ([mcOpenSpace ++ _LIMA_ENABLE ++ mcCloseSpace] ++ ["" | inText] ++ closeTextIf inText ++ res)- else -- in a comment- convert False False True hs (h : res)+ else -- copy lines+ convert False False True dedent_ hs (h : res) | not inSnippet = if | -- found comments h `startsWith` (mdcOpenSpace ++ _LIMA_DISABLE) ->- (convert False False True hs)- ([mcOpenSpace ++ _LIMA_DISABLE ++ mcCloseSpace] ++ ["" | inText] ++ closeTextIf inText ++ res)+ (convert False False True dedent_ hs)+ ([mcOpenSpace ++ _LIMA_DISABLE ++ mcCloseSpace, ""] ++ closeTextIf inText ++ res) | -- found a special comment h `startsWithAnyOf` ((mdcOpenSpace ++) <$> specialComments_) ->- (convert False False False hs)- ([mcOpenSpace ++ stripMdc h ++ mcCloseSpace] ++ ["" | inText] ++ closeTextIf inText ++ res)+ (convert False False False dedent_ hs)+ ([mcOpenSpace ++ stripMdc h ++ mcCloseSpace] ++ [""] ++ closeTextIf inText ++ dropEmpties res) | -- start of a haskell snippet- h `startsWith` backticksHs ->- convert False True False hs (closeTextIf inText ++ squashEmpties res)+ (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 hs res+ convert inText False False dedent_ hs res else -- start of text- convert True False False hs ([h, mcOpen, ""] ++ res)+ convert True False False dedent_ hs ([h, mcOpen, ""] ++ res) | -- copy text line by line otherwise ->- convert True False False hs (h : res)+ convert True False False dedent_ hs (h : res) | otherwise =- if h == backticks+ if (dropSpaces h) == backticks then -- end of a snippet- convert False False False hs res+ convert False False False 0 hs res else -- in a snippet- convert False True False hs (h : res)- convert inText _ _ [] res = [mcClose | inText] ++ res+ convert False True False dedent_ hs ((drop dedent_ h) : res)+ convert inText _ _ dedent_ [] res = [mcClose | inText] ++ res