yamlet-1.0.0.0: src/Yamlet/Internal/Encoder.hs
{-# OPTIONS_HADDOCK not-home #-}
-- | The renderer of the nodes of the encoder.
--
-- This module is intended for internal use only, and may change without warning
-- in subsequent releases.
module Yamlet.Internal.Encoder
( renderDocuments
) where
import Data.Maybe
import Data.Text qualified as T
import Data.Text.Builder.Linear qualified as B
import Yamlet.Internal.Emit
import Yamlet.Internal.Render
import Yamlet.Internal.Syntax qualified as S
import Yamlet.Internal.Utils
-- | Render documents. Documents after the first one start with a @---@
-- marker. The collections of 'Yamlet.Encode.toYaml' are in the block style.
--
-- A document with comments, anchors, aliases, flow collections, scalars on
-- several lines or scalar styles that 'Yamlet.Encode.toYaml' does not create
-- goes to 'Yamlet.Syntax.renderSyntax'. Other documents go to a faster
-- renderer, which gives the same output.
renderDocuments :: [S.Node] -> T.Text
renderDocuments docs
| all simple docs = B.runBuilder . mconcat $ zipWith document [0 :: Int ..] docs
| otherwise = renderSyntax defaultRenderOptions (map S.document docs)
where
document :: Int -> S.Node -> B.Builder
document i n
| null handles = (if i > 0 then "---\n" else mempty) <> topLevel n
| otherwise =
(if i > 0 then "...\n" else mempty)
<> foldMap tagDirective handles
<> "---\n"
<> topLevel n
where
handles :: [Char]
handles = tagHandles n
topLevel :: S.Node -> B.Builder
topLevel n = case n.content of
S.SequenceContent _ xs@(_ : _) -> tagLine n <> blockSequence 0 True xs
S.MappingContent _ kvs@(_ : _) -> tagLine n <> blockMapping 0 True kvs
S.ScalarContent S.Literal t
| needsIndentIndicator t -> withTag n (doubleQuoted t) <> "\n"
_ -> inlineValue indentStep n <> "\n"
-- A tag of a block collection takes a line of its own.
tagLine :: S.Node -> B.Builder
tagLine n = case tagPrefix n of
Just t -> t <> "\n"
Nothing -> mempty
-- | The node has no comments, anchors, aliases and flow collections, and its
-- scalars are on one line and have the styles that 'Yamlet.Encode.toYaml'
-- creates.
simple :: S.Node -> Bool
simple n =
null n.comments.before
&& isNothing n.comments.inline
&& null n.comments.after
&& isNothing n.props.anchor
&& n.props.tag /= S.NonSpecificTag
&& case n.content of
S.ScalarLinesContent _ _ (_ : _) -> False
-- The renderer gives an empty plain scalar no text.
S.ScalarContent S.Plain t -> not (T.null t)
S.ScalarContent S.SingleQuoted _ -> True
S.ScalarContent S.DoubleQuoted _ -> True
S.ScalarContent S.Literal _ -> True
S.ScalarContent _ _ -> False
S.SequenceContent style xs -> (style == S.Block || null xs) && all simple xs
S.MappingContent style kvs ->
(style == S.Block || null kvs) && all (\(k, v) -> simple k && simple v) kvs
S.AliasContent _ -> False
-- | A block sequence of the items of a 'simple' node. The first entry does
-- not start with indentation if the sequence continues a line.
blockSequence :: Int -> Bool -> [S.Node] -> B.Builder
blockSequence indent atLineStart = mconcat . zipWith entry [0 :: Int ..]
where
entry :: Int -> S.Node -> B.Builder
entry i x =
(if i > 0 || atLineStart then spaces indent else mempty)
<> "-"
<> afterIndicator indent x
-- | A node after the indicator of a sequence item or an explicit entry at the
-- given indentation, with the line break. A block collection starts on the
-- line of the indicator, unless it has a tag.
afterIndicator :: Int -> S.Node -> B.Builder
afterIndicator indent x = case x.content of
S.SequenceContent _ xs@(_ : _) ->
collection $ blockSequence (indent + indentStep) False xs
S.MappingContent _ kvs@(_ : _) ->
collection $ blockMapping (indent + indentStep) False kvs
_ -> " " <> inlineValue (indent + indentStep) x <> "\n"
where
collection :: B.Builder -> B.Builder
collection body = case tagPrefix x of
Just t -> " " <> t <> "\n" <> spaces (indent + indentStep) <> body
Nothing -> " " <> body
-- | A block mapping of the entries of a 'simple' node. The first entry does
-- not start with indentation if the mapping continues a line.
blockMapping :: Int -> Bool -> [(S.Node, S.Node)] -> B.Builder
blockMapping indent atLineStart = mconcat . zipWith entry [0 :: Int ..]
where
entry :: Int -> (S.Node, S.Node) -> B.Builder
entry i (k, v) =
(if i > 0 || atLineStart then spaces indent else mempty) <> case implicitKey k of
Just key -> key <> ":" <> value v
Nothing ->
"?"
<> afterIndicator indent k
<> spaces indent
<> ":"
<> afterIndicator indent v
value :: S.Node -> B.Builder
value v = case v.content of
S.SequenceContent _ xs@(_ : _) -> tagged v <> "\n" <> blockSequence indent True xs
S.MappingContent _ kvs@(_ : _) ->
tagged v <> "\n" <> blockMapping (indent + indentStep) True kvs
_ -> " " <> inlineValue (indent + indentStep) v <> "\n"
tagged :: S.Node -> B.Builder
tagged x = maybe mempty (" " <>) (tagPrefix x)
-- A key that fits on one line, or 'Nothing' if it needs an explicit entry.
implicitKey :: S.Node -> Maybe B.Builder
implicitKey k = case k.content of
S.ScalarContent style t
| S.NoTag <- k.props.tag
, style == S.Plain
, plainSyntax False t ->
if T.length t > maxImplicitKeyLength then Nothing else Just (B.fromText t)
| otherwise -> fits (withTag k (scalarText style t))
-- go-yaml v2 reads "[]: a" and "{}: a" without an error, but as an
-- empty list or mapping, and drops the entries after them. A tag
-- avoids that.
S.SequenceContent _ [] | isJust (tagPrefix k) -> fits (inlineValue 0 k)
S.MappingContent _ [] | isJust (tagPrefix k) -> fits (inlineValue 0 k)
_ -> Nothing
where
fits :: B.Builder -> Maybe B.Builder
fits key =
if T.length (B.runBuilder key) > maxImplicitKeyLength then Nothing else Just key
-- | A scalar, or an empty collection in the flow style.
inlineValue :: Int -> S.Node -> B.Builder
inlineValue indent n = withTag n $ case n.content of
S.SequenceContent _ _ -> "[]"
S.MappingContent _ _ -> "{}"
S.ScalarContent S.Literal t | Just (h, b) <- literalBlock indent t -> h <> b
S.ScalarContent style t -> scalarText style t
S.AliasContent _ -> mempty
-- | Prefix the tag if the node has one.
withTag :: S.Node -> B.Builder -> B.Builder
withTag n b = case tagPrefix n of
Just t -> t <> " " <> b
Nothing -> b
tagPrefix :: S.Node -> Maybe B.Builder
tagPrefix n = case n.props.tag of
S.Tag t -> Just (tagText t)
_ -> Nothing
-- | A scalar of a 'simple' node on one line.
scalarText :: S.ScalarStyle -> T.Text -> B.Builder
scalarText style t = case style of
S.Plain
| plainSyntax False t -> B.fromText t
| otherwise -> quotedPlain t
S.SingleQuoted -> quoted
_ -> doubleQuoted t
where
quoted :: B.Builder
quoted = fromMaybe (doubleQuoted t) (singleQuoted t)