summaryrefslogtreecommitdiff
path: root/src/Language/Fiddle/Compiler/Backend/Internal/FormattedWriter.hs
blob: bde9ebed7576161b52c07cb189bbb1b8756c8026 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}

-- | This module provides the 'FormattedWriter' monad, which extends a basic
-- "Writer" over 'Text' by allowing formatting features like indentation, line 
-- breaks, and ensuring the output is well-structured for readability. 
-- 
-- The 'FormattedWriter' is useful for generating pretty-printed output, such as
-- source code generation, where indentation and newline management are essential.
module Language.Fiddle.Compiler.Backend.Internal.FormattedWriter
  ( -- * Types
    FormattedWriter,       -- | The main writer monad with formatting features.
    -- * Core Operations
    ensureNL,              -- | Ensures that there is a newline at the end of the current line.
    flush,                 -- | Flushes any pending text (writes the pending line).
    incIndent,             -- | Increases the indentation level.
    decIndent,             -- | Decreases the indentation level.
    indented,              -- | Performs an action with increased indentation.
    execFormattedWriter,   -- | Runs the 'FormattedWriter' and produces the final formatted 'Text'.
  )
where

import Control.Monad (forM_)
import Control.Monad.RWS.Strict
import Data.Text (Text)
import qualified Data.Text as Text

-- | Internal state for the 'FormattedWriter' monad.
-- 
-- * 'indentLevel': The current level of indentation.
-- * 'pendingLine': The current line that is being built but not yet written.
data Fmt = Fmt
  { indentLevel :: Int,    -- ^ Current indentation level.
    pendingLine :: Text    -- ^ Text that has been added but not yet written to the output.
  }

-- | The 'FormattedWriter' is a monad that provides functionality for writing
-- formatted text, with control over indentation and newlines.
newtype FormattedWriter a = FormattedWriter (RWS () Text Fmt a)
  deriving newtype (Functor, Applicative, Monad, MonadState Fmt)

-- | 'MonadWriter' instance for 'FormattedWriter', which allows writing text
-- while maintaining proper indentation and ensuring that newlines are handled
-- correctly. Each line is prefixed with the correct indentation level.
instance MonadWriter Text FormattedWriter where
  tell txt = FormattedWriter $ do
    -- Get the current indentation level and prepare the indentation prefix.
    indent <- (`Text.replicate` "  ") <$> gets indentLevel
    -- Split the text into lines.
    let lines = Text.splitOn "\n" txt
    -- For each line, write the indent, the pending line, and then the line itself.
    forM_ (Prelude.init lines) $ \line -> do
      pending <- gets pendingLine
      modify $ \s -> s {pendingLine = ""}  -- Reset pending line
      tell indent  -- Add indentation
      tell pending -- Write any pending text
      tell line    -- Write the actual line
      tell "\n"    -- Add a newline after the line
    -- The last fragment is kept as the new pending line, to be written later.
    modify $ \s -> s {pendingLine = pendingLine s <> Prelude.last lines}

  -- Allow listening to the written text within the 'FormattedWriter' context.
  listen (FormattedWriter fn) = FormattedWriter $ listen fn

  -- Allow transformation of the written text via a pass operation.
  pass (FormattedWriter fn) = FormattedWriter $ pass fn

-- | Ensures that a newline is written if there is any pending text. This is
-- useful for ensuring that the output ends on a clean line.
ensureNL :: FormattedWriter ()
ensureNL = do
  p <- gets pendingLine
  if Text.null p
    then return ()
    else do
      modify $ \s -> s {pendingLine = ""}
      tell p
      tell "\n"

-- | Flushes the pending line by writing it to the output and clearing the
-- pending state. This is useful when you want to force the current line to
-- be written immediately.
flush :: FormattedWriter ()
flush = do
  p <- gets pendingLine
  modify $ \s -> s {pendingLine = ""}
  tell p

-- | Increases the indentation level by one. This will affect all subsequent
-- lines written within the 'FormattedWriter' monad.
incIndent :: FormattedWriter ()
incIndent = modify (\(Fmt id p) -> Fmt (id + 1) p)

-- | Decreases the indentation level by one. This will affect all subsequent
-- lines written within the 'FormattedWriter' monad.
decIndent :: FormattedWriter ()
decIndent = modify (\(Fmt id p) -> Fmt (id - 1) p)

-- | Runs the given 'FormattedWriter' action with an additional indentation level.
-- Once the action completes, the indentation level is decreased.
indented :: FormattedWriter () -> FormattedWriter ()
indented fn = do
  incIndent  -- Increase indentation level
  fn         -- Run the action with the increased indentation
  decIndent  -- Restore the original indentation level

-- | Runs a 'FormattedWriter' and returns the final formatted 'Text' output.
-- It ensures that any pending lines are flushed before returning the result.
execFormattedWriter :: FormattedWriter a -> Text
execFormattedWriter ((>> flush) -> FormattedWriter rws) =
  snd $ execRWS rws () (Fmt 0 "")  -- Execute the RWS monad, starting with no indentation and no pending line.