packages feed

hpack-0.39.6: src/Hpack/Render/Dsl.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Hpack.Render.Dsl (
-- * AST
  Element (..)
, Value (..)

-- * Render
, RenderSettings (..)
, CommaStyle (..)
, defaultRenderSettings
, Alignment (..)
, Nesting
, render

-- * Utils
, sortFieldsBy

#ifdef TEST
, Lines (..)
, IndentOrAlign (..)
, renderValue
, addSortKey
#endif
) where

import           Imports
import           Data.Char (isSpace)

data Element = Stanza String [Element] | Group Element Element | Field String Value | Verbatim String
  deriving (Eq, Show)

data Value =
    Literal String
  | CommaSeparatedList [String]
  | LineSeparatedList [String]
  | WordList [String]
  deriving (Eq, Show)

data Lines = SingleLine String | MultipleLines IndentOrAlign [String]
  deriving (Eq, Show)

data IndentOrAlign =
  Indent
  -- ^
  -- Indent lines, e.g.
  --
  -- description:
  --   some
  --   multiline
  --   description
  |
  Align
  -- ^
  -- Align lines with field labels, e.g.
  --
  -- description: some
  --              multiline
  --              description
  deriving (Eq, Show)

data CommaStyle = LeadingCommas | TrailingCommas
  deriving (Eq, Show)

newtype Nesting = Nesting Int
  deriving (Eq, Show, Num, Enum)

newtype Alignment = Alignment Int
  deriving (Eq, Ord, Show, Num)

data RenderSettings = RenderSettings {
  renderSettingsIndentation :: Int
, renderSettingsFieldAlignment :: Alignment
, renderSettingsCommaStyle :: CommaStyle
, renderSettingsEmptyLinesAsDot :: Bool
} deriving (Eq, Show)

defaultRenderSettings :: RenderSettings
defaultRenderSettings = RenderSettings 2 0 LeadingCommas True

render :: RenderSettings -> Nesting -> Element -> [String]
render settings nesting = \ case
  Stanza name elements -> indent settings nesting name : renderElements settings (succ nesting) elements
  Group a b -> render settings nesting a ++ render settings nesting b
  Field name value -> map (indent settings nesting) $ renderField settings name value
  Verbatim str -> map (indent settings nesting) (lines str)

renderElements :: RenderSettings -> Nesting -> [Element] -> [String]
renderElements settings nesting = concatMap (render settings nesting)

renderField :: RenderSettings -> String -> Value -> [String]
renderField settings@RenderSettings{..} name = renderValue settings >>> \ case
  SingleLine "" -> []
  SingleLine value -> [fieldName ++ value]
  MultipleLines _ [] -> []
  MultipleLines Indent values -> (name ++ ":") : map (indent settings 1) values
  MultipleLines Align (value : values) -> (fieldName ++ value) : map align values
  where
    Alignment fieldAlignment = renderSettingsFieldAlignment

    fieldName :: String
    fieldName = name ++ ": " ++ fieldNamePadding

    fieldNamePadding :: String
    fieldNamePadding = replicate (fieldAlignment - length name - 2) ' '

    align :: String -> String
    align = \ case
      "" -> ""
      value -> padding ++ value

    padding :: String
    padding = replicate (length fieldName) ' '

renderValue :: RenderSettings -> Value -> Lines
renderValue RenderSettings{..} = \ case
  Literal string -> case lines string of
    [value] -> SingleLine value
    values -> MultipleLines Align $ map emptyLineToDot values
  WordList ws -> SingleLine $ unwords ws
  LineSeparatedList xs -> renderLineSeparatedList renderSettingsCommaStyle xs
  CommaSeparatedList xs -> renderCommaSeparatedList renderSettingsCommaStyle xs
  where
    emptyLineToDot :: String -> String
    emptyLineToDot xs
      | isEmptyLine xs && renderSettingsEmptyLinesAsDot = "."
      | otherwise = xs

    isEmptyLine :: String -> Bool
    isEmptyLine = all isSpace

renderLineSeparatedList :: CommaStyle -> [String] -> Lines
renderLineSeparatedList style = MultipleLines Indent . map (padding ++)
  where
    padding = case style of
      LeadingCommas -> "  "
      TrailingCommas -> ""

renderCommaSeparatedList :: CommaStyle -> [String] -> Lines
renderCommaSeparatedList style = MultipleLines Indent . case style of
  LeadingCommas -> map renderLeadingComma . zip (True : repeat False)
  TrailingCommas -> map renderTrailingComma . reverse . zip (True : repeat False) . reverse
  where
    renderLeadingComma :: (Bool, String) -> String
    renderLeadingComma (isFirst, x)
      | isFirst   = "  " ++ x
      | otherwise = ", " ++ x

    renderTrailingComma :: (Bool, String) -> String
    renderTrailingComma (isLast, x)
      | isLast    = x
      | otherwise = x ++ ","

instance IsString Value where
  fromString = Literal

indent :: RenderSettings -> Nesting -> String -> String
indent RenderSettings{..} (Nesting nesting) = \ case
  "" -> ""
  s -> replicate (nesting * renderSettingsIndentation) ' ' ++ s

sortFieldsBy :: [String] -> [Element] -> [Element]
sortFieldsBy existingFieldOrder =
    map snd
  . sortOn fst
  . addSortKey
  . map (\a -> (existingIndex a, a))
  where
    existingIndex :: Element -> Maybe Int
    existingIndex (Field name _) = name `elemIndex` existingFieldOrder
    existingIndex _ = Nothing

addSortKey :: [(Maybe Int, a)] -> [((Int, Int), a)]
addSortKey = go (-1) . zip [0..]
  where
    go :: Int -> [(Int, (Maybe Int, a))] -> [((Int, Int), a)]
    go n xs = case xs of
      [] -> []
      (x, (Just y, a)) : ys -> ((y, x), a) : go y ys
      (x, (Nothing, a)) : ys -> ((n, x), a) : go n ys