lima-0.2.1.2: src/Converter.hs
-- |
-- == 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 'Tokens'. 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'.
--
-- There are several forms of @Haskell@ code blocks in @Literate Haskell@ recognized by @GHC@.
--
-- 1. Code between @\begin{code}@ and @\end{code}@ tags.
--
-- @
-- \begin{code}
-- a = 42
-- \end{code}
-- \begin{code}
-- b = a
-- \end{code}
-- @
--
-- - The line starting with @\begin{code}@ cannot have other non-space characters after @\begin{code}@.
-- - The indentation of all expressions in code blocks must be the same.
--
-- 1. Code lines starting with @'> '@.
--
-- @
-- \begin{mycode}
--
-- > a = 42
--
-- \end{mycode}
-- \begin{mycode}
--
-- > b = a
--
-- \end{mycode}
-- @
--
-- - There must be at least a single empty line before and after each @Haskell@ code block.
-- - Any text may surround @Haskell@ code blocks.
-- - The indentation of all expressions in code blocks must be the same.
--
-- This library supports only the second form as this form is more versatile.
--
-- Moreover, this form does not require writing @Markdown@ tags like @\'```haskell\'@.
--
-- Such tags will automatically be printed when converting @Literate Haskell@ to @Markdown@.
module Converter (
-- * Config
Mode,
User,
Internal,
Config (..),
def,
toConfigInternal,
-- ** Lenses
disable,
enable,
indent,
dedent,
mdHaskellCodeStart,
mdHaskellCodeEnd,
texHaskellCodeStart,
texHaskellCodeEnd,
texSingleLineCommentStart,
lhsSingleLineCommentStart,
-- * microlens
(&),
(?~),
-- * Format
Format (..),
convertTo,
showFormatExtension,
showFormatName,
-- * Tokens
Token (..),
Tokens,
selectFromTokens,
selectToTokens,
mergeTokens,
stripTokens,
normalizeTokens,
-- * Printers
hsFromTokens,
hsFromTokens',
lhsFromTokens,
lhsFromTokens',
mdFromTokens,
mdFromTokens',
texFromTokens,
texFromTokens',
-- * Parsers
hsToTokens,
lhsToTokens,
mdToTokens,
texToTokens,
-- * Helpers
mkFromTokens,
mkToTokens,
parseLineToToken,
errorExpectedToken,
errorNotEnoughTokens,
pp,
-- * Examples
exampleNonTexTokens',
exampleNonTexTokens,
exampleTexTokens,
) where
import Converter.Internal (Pretty (..), PrettyPrint (..), constructorName, countSpaces, dropEmpties, dropLen, dropLhsComment, dropTexComment, errorEmptyCommentAt, hsCommentClose, hsCommentCloseSpace, hsCommentOpen, hsCommentOpenSpace, indentN, isHsComment, isMdComment, lhsCommentSpace, lhsEscapeHash, lhsHsCodeSpace, lhsUnescapeHash, mdCommentClose, mdCommentCloseSpace, mdCommentOpenSpace, prependLhsComment, prependTexComment, startsWith, stripEmpties, stripHsComment, stripMdComment, stripSpaces, texCommentSpace)
import Data.Char (isAlpha)
import Data.Data (Data)
import Data.Default (Default (def))
import Data.List (intersperse)
import Data.List.NonEmpty as NonEmpty (NonEmpty ((:|)), fromList, init, last, toList, (<|))
import Data.Text qualified as T
import GHC.Generics (Generic)
import Lens.Micro (non, to, (&), (?~), (^.))
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 where
Mode User = Maybe String
Mode Internal = T.Text
-- | Configuration of tag names.
--
-- The default values of @Config User@ are all 'Nothing's.
--
-- Inside the library functions, @Config User@ is converted to @Config Internal@.
--
-- The below examples show the names from @Config Internal@.
--
-- >>> pp (def :: Config User)
-- Config {
-- _disable = "LIMA_DISABLE",
-- _enable = "LIMA_ENABLE",
-- _indent = "LIMA_INDENT",
-- _dedent = "LIMA_DEDENT",
-- _mdHaskellCodeStart = "```haskell",
-- _mdHaskellCodeEnd = "```",
-- _texHaskellCodeStart = "\\begin{mycode}",
-- _texHaskellCodeEnd = "\\end{mycode}",
-- _texSingleLineCommentStart = "SINGLE_LINE ",
-- _lhsSingleLineCommentStart = "SINGLE_LINE "
-- }
--
-- It's possible to override these names.
--
-- >>> pp ((def :: Config User) & disable ?~ "off" & enable ?~ "on" & indent ?~ "indent" & dedent ?~ "dedent")
-- Config {
-- _disable = "off",
-- _enable = "on",
-- _indent = "indent",
-- _dedent = "dedent",
-- _mdHaskellCodeStart = "```haskell",
-- _mdHaskellCodeEnd = "```",
-- _texHaskellCodeStart = "\\begin{mycode}",
-- _texHaskellCodeEnd = "\\end{mycode}",
-- _texSingleLineCommentStart = "SINGLE_LINE ",
-- _lhsSingleLineCommentStart = "SINGLE_LINE "
-- }
data Config (a :: Mode') = Config
{ _disable :: Mode a
-- ^
-- Make parser ignore tags and just copy the following lines verbatim.
--
-- Set indentation to @0@.
, _enable :: Mode a
-- ^ Stop parser from ignoring tags.
, _indent :: Mode a
-- ^ Set code indentation to a given 'Int'.
, _dedent :: Mode a
-- ^ Set code indentation to @0@.
, _mdHaskellCodeStart :: Mode a
-- ^ Mark the start of a @Haskell@ code block in @Markdown@.
, _mdHaskellCodeEnd :: Mode a
-- ^ Mark the end of a @Haskell@ code block in @Markdown@.
, _texHaskellCodeStart :: Mode a
-- ^ Mark the start of a @Haskell@ code block in @TeX@.
, _texHaskellCodeEnd :: Mode a
-- ^ Mark the end of a @Haskell@ code block in @TeX@.
, _texSingleLineCommentStart :: Mode a
-- ^ Mark start of a comment that must be single-line in @TeX@.
, _lhsSingleLineCommentStart :: Mode a
-- ^ Mark start of a comment that must be single-line in @Literate Haskell@.
}
deriving (Generic)
makeLenses ''Config
deriving instance Show (Config User)
deriving instance Eq (Config User)
deriving instance Show (Config Internal)
instance PrettyPrint (Config User) where
pp :: Config User -> Pretty String
pp (toConfigInternal -> 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{mycode}"
_texHaskellCodeEnd = "\\end{mycode}"
_texSingleLineCommentStart = "SINGLE_LINE"
_lhsSingleLineCommentStart = "SINGLE_LINE"
deriving instance Default (Config User)
-- | Convert a user 'Config' to an internal 'Config' with user-supplied values.
--
-- It's important to do this conversion at a single entrypoint.
--
-- Otherwise, repeated conversions will accumulate changes such as appended spaces.
toConfigInternal :: Config User -> Config Internal
toConfigInternal conf =
Config
{ _disable = l disable _disable
, _enable = l enable _enable
, _indent = l indent _indent
, _dedent = l dedent _dedent
, _mdHaskellCodeStart = l mdHaskellCodeStart _mdHaskellCodeStart
, _mdHaskellCodeEnd = l mdHaskellCodeEnd _mdHaskellCodeEnd
, _texHaskellCodeStart = l texHaskellCodeStart _texHaskellCodeStart
, _texHaskellCodeEnd = l texHaskellCodeEnd _texHaskellCodeEnd
, _texSingleLineCommentStart = (l texSingleLineCommentStart _texSingleLineCommentStart) <> " "
, _lhsSingleLineCommentStart = (l lhsSingleLineCommentStart _lhsSingleLineCommentStart) <> " "
}
where
l a b = conf ^. a . to (T.pack <$>) . non b
Config{..} = def @(Config Internal)
-- | A format of a document.
data Format
= -- | @Haskell@
Hs
| -- | @Literate Haskell@
Lhs
| -- | @Markdown@
Md
| -- | @TeX@
TeX
deriving (Eq)
-- | 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"
-- | Internal representation of a document.
--
-- A printer processes 'Tokens' one by one.
--
-- A 'Token' can have:
--
-- - Action - how this 'Token' affects the subsequent 'Tokens'.
-- - Target - a type of 'Tokens' that are affected by this 'Token'.
-- - Range - the nearest 'Token' until which this 'Token' affects the subsequent 'Tokens'.
data Token
= -- |
-- - Action: set indentation to @n@.
--
-- - Target: 'HaskellCode'.
--
-- - Range: 'Indent', 'Dedent', or 'Disabled'.
Indent {n :: Int}
| -- |
-- - Action: set indentation to @0@.
--
-- - Target: 'HaskellCode'.
--
-- - Range: 'Indent', 'Dedent', or 'Disabled'.
Dedent
| -- | A block that should be invisible when rendered outside of @.hs@.
--
-- - Action: set indentation to @0@.
--
-- - Target: 'HaskellCode'.
--
-- - Range: '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}
| -- | A line of a comment that must be kept on a single-line.
--
-- E.g., {- FOURMOLU_DISABLE -} from a @.hs@.
CommentSingleLine {someLine :: 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]"
-- | 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
-- | 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!" :| []},
-- CommentSingleLine {someLine = "Comment on a single line."},
-- Text {someLines = "Hello from text," :| []},
-- Text {someLines = "world!" :| []}
-- ]
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!" :| [])
, CommentSingleLine ("Comment on a single line.")
, Text ("Hello from text," :| [])
, Text ("world!" :| [])
]
-- | 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!" :| []},
-- CommentSingleLine {someLine = "Comment on a single line."},
-- Text {someLines = "Hello from text," :| []},
-- Text {someLines = "world!" :| []}
-- ]
--
-- >>> 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,"]},
-- CommentSingleLine {someLine = "Comment on a single line."},
-- Text {someLines = "world!" :| ["","Hello from text,"]}
-- ]
mergeTokens :: Tokens -> Tokens
mergeTokens (t1@Text{} : t2@Text{} : ts) = mergeTokens $ Text{someLines = someLines t2 <> (T.empty <| someLines t1)} : ts
mergeTokens (Comment{someLines = ls1} : Comment{someLines = ls2} : ts) =
mergeTokens $ Comment{someLines = ls2 <> (T.empty <| ls1)} : ts
mergeTokens (t : ts) = t : mergeTokens ts
mergeTokens ts = ts
-- | Strip empty lines and 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!" :| []},
-- CommentSingleLine {someLine = "Comment on a single line."},
-- Text {someLines = "Hello from text," :| []},
-- Text {someLines = "world!" :| []}
-- ]
--
-- >>> 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!" :| []},
-- CommentSingleLine {someLine = "Comment on a single line."},
-- Text {someLines = "Hello from text," :| []},
-- Text {someLines = "world!" :| []}
-- ]
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
-- | '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,"]},
-- CommentSingleLine {someLine = "Comment on a single line."},
-- Text {someLines = "world!" :| ["","Hello from text,"]}
-- ]
normalizeTokens :: Tokens -> Tokens
normalizeTokens tokens = stripTokens $ mergeTokens $ tokens
-- | 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,"]},
-- CommentSingleLine {someLine = "Comment on a single line."},
-- Text {someLines = "world!" :| ["","Hello from text,"]}
-- ]
exampleNonTexTokens :: Tokens
exampleNonTexTokens = normalizeTokens exampleNonTexTokens'
-- | same as 'exampleNonTexTokens', but with @TeX@-specific tags that make @Haskell@ code blocks correctly parsable.
--
-- >>> pp $ exampleTexTokens
-- [
-- Indent {n = 3},
-- Disabled {manyLines = ["-- What's the answer?"]},
-- Indent {n = 1},
-- Indent {n = 0},
-- Text {someLines = "\\begin{mycode}" :| ["","Intermediate results"]},
-- HaskellCode {manyLines = ["b = a 4","a = const 3"]},
-- Text {someLines = "\\end{mycode}" :| []},
-- Dedent,
-- Text {someLines = "\\begin{mycode}" :| []},
-- HaskellCode {manyLines = ["answer = b * 14"]},
-- Text {someLines = "\\end{mycode}" :| []},
-- Comment {someLines = "world!" :| ["","Hello from comments,"]},
-- CommentSingleLine {someLine = "Comment on a single line."}
-- ]
exampleTexTokens :: Tokens
exampleTexTokens =
normalizeTokens $
[ Indent 3
, Disabled{manyLines = ["-- What's the answer?"]}
, Indent 1
, Indent 0
, Text{someLines = "Intermediate results" :| []}
, codeStart
, HaskellCode [" b = a 4", " a = const 3"]
, codeEnd
, Dedent
, codeStart
, HaskellCode ["answer = b * 14"]
, codeEnd
, Comment ("Hello from comments," :| [])
, Comment ("world!" :| [])
, CommentSingleLine ("Comment on a single line.")
]
where
codeStart = Text{someLines = def @(Config Internal) ^. texHaskellCodeStart :| []}
codeEnd = Text{someLines = def @(Config Internal) ^. texHaskellCodeEnd :| []}
-- | 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
-- | 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
}
-- | 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
-- | Compose a function from a 'T.Text' to 'Tokens'.
mkToTokens :: (State -> [(Int, T.Text)] -> [Token] -> [Token]) -> T.Text -> Tokens
mkToTokens toTokens xs = normalizeTokens (drop 1 $ reverse $ toTokens def (zip [1 ..] (T.lines xs)) [Dedent])
-- | Parse a single line to a token.
--
-- - Merge comments
parseLineToToken :: Config Internal -> Format -> Token -> T.Text -> Int -> Tokens
parseLineToToken Config{..} format prev l lineNumber
| l `startsWith` _indent =
maybe
(error $ "Expected a number after " <> T.unpack _indent <> " at line: " <> show lineNumber)
(\x -> [Indent (max 0 x), prev])
(readMaybe @Int (T.unpack (dropLen _indent l)))
| l == _dedent = [Dedent, prev]
| format `elem` [Md, Hs] = [CommentSingleLine{someLine = l}, prev]
| format == TeX && l `startsWith` _texSingleLineCommentStart =
[CommentSingleLine{someLine = dropLen _texSingleLineCommentStart l}, prev]
| format == Lhs && l `startsWith` _lhsSingleLineCommentStart =
[CommentSingleLine{someLine = dropLen _lhsSingleLineCommentStart l}, 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")
errorNotEnoughTokens :: Format -> a
errorNotEnoughTokens format = error $ "Got not enough tokens when converting 'Tokens' to " <> showFormatName format
-- | Convert 'Tokens' to @TeX@ code.
--
-- __Rules__
--
-- - Certain [assumptions]("Converter#assumptions") must hold for inputs.
-- - These are the relations between tokens and document blocks when the default 'Config' values are used.
--
-- - 'Indent' ~ @'% LIMA_INDENT N'@ (@N@ is an 'Int').
-- - 'Dedent' ~ @'% LIMA_DEDENT'@.
-- - 'Disabled' ~ @'% LIMA_DISABLE'@ and @'% LIMA_ENABLE'@ and lines between them.
-- - 'CommentSingleLine' ~ a line starting with @'% SINGLE_LINE '@.
--
-- @
-- % SINGLE_LINE line
-- @
--
-- - 'Comment' ~ consecutive lines, either empty or starting with @'% '@.
--
-- @
-- % Hello,
-- % world!
--
-- % Hello,
-- % user!
-- @
--
-- - At least one line must have nonempty text after @'% '@.
--
-- - 'HaskellCode' ~ lines between possibly indented tags @'\\begin{code}'@ and @'\\end{code}'@.
--
-- - Inside a 'Token', code will be shifted to the left. See 'normalizeTokens'.
-- - When printing the 'Tokens', code will be indented according to previous 'Tokens'.
--
-- - 'Text' ~ other lines.
--
-- === __Example__
--
-- >>> pp $ texFromTokens def exampleTexTokens
-- % LIMA_INDENT 3
-- <BLANKLINE>
-- % LIMA_DISABLE
-- <BLANKLINE>
-- % -- What's the answer?
-- <BLANKLINE>
-- % LIMA_ENABLE
-- <BLANKLINE>
-- % LIMA_INDENT 1
-- <BLANKLINE>
-- % LIMA_INDENT 0
-- <BLANKLINE>
-- Intermediate results
-- <BLANKLINE>
-- \begin{mycode}
-- a = const 3
-- b = a 4
-- \end{mycode}
-- <BLANKLINE>
-- % LIMA_DEDENT
-- <BLANKLINE>
-- \begin{mycode}
-- answer = b * 14
-- \end{mycode}
-- <BLANKLINE>
-- % Hello from comments,
-- <BLANKLINE>
-- % world!
-- <BLANKLINE>
-- % SINGLE_LINE Comment on a single line.
texFromTokens :: Config User -> Tokens -> T.Text
texFromTokens = mkFromTokens texFromTokens'
-- | 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' (toConfigInternal -> 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)
CommentSingleLine{someLine} -> (curIndent, [prependTexComment $ _texSingleLineCommentStart <> someLine] : [] : rs)
translate _ _ _ = errorNotEnoughTokens TeX
-- | Convert 'Tokens' to @TeX@ code.
--
-- Inverse of 'texFromTokens'.
--
-- >>> (texToTokens def $ texFromTokens def exampleTexTokens) == exampleTexTokens
-- True
texToTokens :: Config User -> T.Text -> Tokens
texToTokens (toConfigInternal -> conf@Config{..}) xs = tokens
where
tokens = mkToTokens 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 $
parseLineToToken conf TeX 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.
--
-- - 'Indent' ~ @'% LIMA_INDENT N'@ (@N@ is an 'Int').
-- - 'Dedent' ~ @'% LIMA_DEDENT'@.
-- - 'Disabled' ~ Lines between and including @'% LIMA_DISABLE'@ and @'% LIMA_ENABLE'@.
--
-- - There must be at least one nonempty line between these tags.
--
-- - 'CommentSingleLine' ~ a line starting with @'% SINGLE_LINE '@.
--
-- @
-- % SINGLE_LINE line
-- @
--
-- - 'Comment' ~ consecutive lines, either empty or starting with @'% '@.
--
-- @
-- % Hello,
-- % world!
--
-- % Hello,
-- % user!
-- @
--
-- - At least one line must have nonempty text after @'% '@
--
-- - 'HaskellCode' ~ consecutive lines starting with @'> '@.
--
-- @
-- > a4 = 4
-- > a2 = 2
-- @
--
-- - Inside a 'Token', code is shifted to the left. See 'normalizeTokens'.
-- - During printing, code is indented according to previous 'Tokens'.
--
-- - 'Text' ~ other lines.
--
-- === __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
-- <BLANKLINE>
-- > a = const 3
-- > b = a 4
-- <BLANKLINE>
-- % LIMA_DEDENT
-- <BLANKLINE>
-- > answer = b * 14
-- <BLANKLINE>
-- % Hello from comments,
-- <BLANKLINE>
-- % world!
-- <BLANKLINE>
-- % SINGLE_LINE Comment on a single line.
-- <BLANKLINE>
-- Hello from text,
-- <BLANKLINE>
-- world!
lhsFromTokens :: Config User -> Tokens -> T.Text
lhsFromTokens config tokens = mkFromTokens lhsFromTokens' config tokens
-- | 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' (toConfigInternal -> Config{..}) blocks =
dropEmpties $ reverse (T.intercalate "\n" . reverse <$> (fromTokens (blocks) (0, [])))
where
fromTokens :: Tokens -> (Int, [[T.Text]]) -> [[T.Text]]
fromTokens (cur : bs) (curIndent, rs) =
fromTokens bs (translate curIndent (cur : bs) rs)
fromTokens [] (_, rs) = rs
translate curIndent (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) : [] : rs)
Text{..} -> (curIndent, toList (lhsEscapeHash <$> someLines) : [] : rs)
Comment{someLines = t :| ts} -> (curIndent, (prependLhsComment <$> t : ts) : [] : rs)
CommentSingleLine{someLine} -> (curIndent, [prependLhsComment $ _lhsSingleLineCommentStart <> someLine] : [] : rs)
translate _ _ _ = errorNotEnoughTokens Lhs
-- | Convert 'Tokens' to @Markdown@ code.
--
-- Inverse of 'lhsFromTokens'.
--
-- >>> (lhsToTokens def $ lhsFromTokens def exampleNonTexTokens) == exampleNonTexTokens
-- True
lhsToTokens :: Config User -> T.Text -> Tokens
lhsToTokens (toConfigInternal -> conf@Config{..}) xs = tokens
where
tokens = mkToTokens 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 $
parseLineToToken conf Lhs 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.
--
-- - 'Indent' ~ @'<!-- LIMA_INDENT N --\>'@, where @N@ is an 'Int'.
-- - 'Dedent' ~ @'<!-- LIMA_DEDENT --\>'@.
-- - 'Disabled' ~ a multiline comment
-- starting with @'<!-- LIMA_DISABLE\\n'@
-- and ending with @'\\nLIMA_ENABLE --\>'@.
--
-- @
-- <!-- LIMA_DISABLE
-- a4 = 4
-- a2 = 2
-- LIMA_ENABLE --\>
-- @
--
-- - 'CommentSingleLine' ~ a line starting with @'<!-- '@ and ending with @' -->'@.
--
-- @
-- <!-- line -->
-- @
--
-- - 'Comment' ~ a multiline comment starting with @'<!-- {text}'@, where @{text}@ is nonempty text.
--
-- @
-- <!-- line 1
-- line 2
-- --\>
-- @
--
-- - Consecutive 'Comment's are merged into a single 'Comment'.
--
-- - 'HaskellCode' ~ possibly indented block starting with @\'```haskell\'@ and ending with @'```'@.
--
-- @
-- ```haskell
-- a4 = 2
-- ```
-- @
--
-- - 'Text' ~ other lines.
--
-- === __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>
-- <!-- Comment on a single line. -->
-- <BLANKLINE>
-- Hello from text,
-- <BLANKLINE>
-- world!
mdFromTokens :: Config User -> Tokens -> T.Text
mdFromTokens = mkFromTokens mdFromTokens'
-- | 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' (toConfigInternal -> 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} ->
fromTokens curIndent bs $
[mdCommentClose] <> NonEmpty.init someLines <> [mdCommentOpenSpace <> NonEmpty.last someLines] : res
CommentSingleLine{someLine} ->
fromTokens curIndent bs $
[mdCommentOpenSpace <> someLine <> mdCommentCloseSpace] : res
-- | Convert 'Tokens' to @Markdown@ code.
--
-- Inverse of 'mdFromTokens'.
--
-- >>> (mdToTokens def $ mdFromTokens def exampleNonTexTokens) == exampleNonTexTokens
-- True
mdToTokens :: Config User -> T.Text -> Tokens
mdToTokens (toConfigInternal -> conf@Config{..}) xs = tokens
where
tokens = mkToTokens 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 $ parseLineToToken conf Md 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 'Tokens' and document blocks when the default 'Config' values are used.
--
-- - 'Indent' ~ @'{- LIMA_INDENT N -}'@ where @N@ is an 'Int'.
-- - 'Dedent' ~ @'{- LIMA_DEDENT -}'@.
-- - 'Disabled' ~ @'{- LIMA_DISABLE -}'@ and @'{- LIMA_ENABLE -}'@ and lines between them.
--
-- @
-- {- LIMA_DISABLE -}
--
-- disabled
--
-- {- LIMA_ENABLE -}
-- @
--
-- - 'Text' ~ a multiline comment starting with @'{-\\n'@ and ending with @'\\n-}'@.
--
-- @
-- {-
-- line 1
-- -}
-- @
--
-- - Consecutive 'Text's are merged into a single 'Text'.
-- - There must be at list one nonempty line inside this comment.
--
-- - 'CommentSingleLine' ~ a multiline comment on a single line.
--
-- @
-- {- line -}
-- @
--
-- - 'Comment' ~ a multiline comment starting with @'{- TEXT'@, where @TEXT@ is nonempty text, and ending with @\\n-}@
--
-- @
-- {- line 1
-- line 2
-- -}
-- @
--
-- - Consecutive 'Comment's are merged into a single 'Comment'.
--
-- - 'HaskellCode' ~ other lines.
--
-- === __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>
-- {- Comment on a single line. -}
-- <BLANKLINE>
-- {-
-- Hello from text,
-- <BLANKLINE>
-- world!
-- -}
hsFromTokens :: Config User -> Tokens -> T.Text
hsFromTokens = mkFromTokens hsFromTokens'
-- | 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' (toConfigInternal -> 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} ->
[hsCommentClose] <> NonEmpty.init someLines <> [hsCommentOpenSpace <> NonEmpty.last someLines] : res
CommentSingleLine{someLine} ->
[hsCommentOpenSpace <> someLine <> hsCommentCloseSpace] : res
-- | Convert 'Tokens' to @Haskell@ code.
--
-- Inverse of 'hsFromTokens'.
--
-- >>> (hsToTokens def $ hsFromTokens def exampleNonTexTokens) == exampleNonTexTokens
-- True
hsToTokens :: Config User -> T.Text -> Tokens
hsToTokens (toConfigInternal -> conf@Config{..}) xs = tokens
where
tokens = mkToTokens 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)
| otherwise -> toTokens def ls $ parseLineToToken conf Hs 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