packages feed

nix-lang-0.1.0.0: src/Nix/Lang/RFCPrint.hs

{-# LANGUAGE EmptyCase #-}

-- | RFC 166-oriented formatter for fresh syntax trees.
--
-- This formatter is for fresh syntax trees: values you constructed yourself,
-- lowered from another representation, or otherwise do not want to exact-print.
--
-- In other words, this is the “choose a clean layout” renderer, not the
-- “preserve the original file exactly” renderer. For the latter, use
-- 'Nix.Lang.ExactPrint'. If you want this formatter to inherit some layout intent
-- from parsed source, use 'Nix.Lang.RFCPrint.LayoutHints'.
--
-- Example:
--
-- @
-- import Nix.Lang.RFCPrint (formatExpr)
-- import Nix.Lang.Types.Syn (mkApp, mkVar)
--
-- doc = formatExpr (mkApp (mkVar "f") (mkVar "x"))
-- @
module Nix.Lang.RFCPrint
  ( formatExpr,
    formatExprWithHints,
    formatDoc,
    formatDocWithHints,
  )
where

import Data.Text (Text)
import qualified Data.Text as T
import Nix.Lang.Lexer.Text (escapeIndentedText, escapedChars)
import Nix.Lang.RFCPrint.LayoutHints
import Nix.Lang.Types
import Nix.Lang.Types.Syn
import Nix.Lang.Utils (showBinOP)
import Prettyprinter
import Prettyprinter.Render.Text (renderStrict)

newtype FormatEnv = FormatEnv
  { feLayoutHints :: LayoutHints
  }

rootPath :: NodePath
rootPath = []

childPath :: NodePath -> Int -> NodePath
childPath = childNodePath

chooseLayout :: FormatEnv -> NodePath -> ContainerLayout -> ContainerLayout
chooseLayout env path defaultLayout =
  maybe defaultLayout resolve (lookupLayoutHint path (feLayoutHints env))
  where
    resolve layoutHint = maybe defaultLayout id (lhContainerLayout layoutHint)

lookupTrivia :: FormatEnv -> NodePath -> ([CanonicalTrivia], [CanonicalTrivia])
lookupTrivia env path =
  case lookupLayoutHint path (feLayoutHints env) of
    Just hint -> (lhPriorTrivia hint, lhFollowingTrivia hint)
    Nothing -> ([], [])

formatInterpolation :: FormatEnv -> NodePath -> Expr -> Doc ann
formatInterpolation env path expr = "${" <> formatTop env path expr <> "}"

forceMultilineEnv :: FormatEnv -> NodePath -> FormatEnv
forceMultilineEnv env path = env {feLayoutHints = forceMultilineHint path (feLayoutHints env)}

formatExpr :: Expr -> Text
formatExpr = formatExprWithHints emptyLayoutHints

formatExprWithHints :: LayoutHints -> Expr -> Text
formatExprWithHints hints = renderStrict . layoutPretty defaultLayoutOptions . formatDocWithHints hints

formatDoc :: Expr -> Doc ann
formatDoc = formatDocWithHints emptyLayoutHints

formatDocWithHints :: LayoutHints -> Expr -> Doc ann
formatDocWithHints hints expr =
  let env = FormatEnv hints
   in formatTop env rootPath expr

formatTop :: FormatEnv -> NodePath -> Expr -> Doc ann
formatTop env path expr =
  let (priorTrivia, followingTrivia) = lookupTrivia env path
   in applyTrivia priorTrivia (formatNode env path expr) followingTrivia

data Assoc
  = AssocLeft
  | AssocRight
  | AssocNone
  deriving (Eq)

precLowest, precImpl, precOr, precAnd, precEq, precCmp, precUpdate, precNot, precAdd, precMul, precConcat, precHasAttr, precNeg, precApp, precSelect, precAtom :: Int
precLowest = 0
precImpl = 1
precOr = 2
precAnd = 3
precEq = 4
precCmp = 5
precUpdate = 6
precNot = 7
precAdd = 8
precMul = 9
precConcat = 10
precHasAttr = 11
precNeg = 12
precApp = 13
precSelect = 14
precAtom = 15

exprPrec :: Expr -> Int
exprPrec = \case
  NixVar {} -> precAtom
  NixLit {} -> precAtom
  NixPar {} -> precAtom
  NixString {} -> precAtom
  NixPath {} -> precAtom
  NixEnvPath {} -> precAtom
  NixSelect _ _ _ (Just _) -> precLowest
  NixSelect {} -> precSelect
  NixApp {} -> precApp
  NixNegApp {} -> precNeg
  NixHasAttr {} -> precHasAttr
  NixBinApp _ op _ _ -> opPrec op
  NixNotApp {} -> precNot
  NixList {} -> precAtom
  NixSet {} -> precAtom
  NixLet {} -> precLowest
  NixIf {} -> precLowest
  NixWith {} -> precLowest
  NixAssert {} -> precLowest
  NixLam {} -> precLowest

opPrec :: BinaryOp -> Int
opPrec = \case
  OpConcat -> precConcat
  OpMul -> precMul
  OpDiv -> precMul
  OpAdd -> precAdd
  OpSub -> precAdd
  OpUpdate -> precUpdate
  OpLT -> precCmp
  OpLE -> precCmp
  OpGT -> precCmp
  OpGE -> precCmp
  OpEq -> precEq
  OpNEq -> precEq
  OpAnd -> precAnd
  OpOr -> precOr
  OpImpl -> precImpl

opAssoc :: BinaryOp -> Assoc
opAssoc = \case
  OpConcat -> AssocRight
  OpUpdate -> AssocRight
  OpImpl -> AssocRight
  OpEq -> AssocNone
  OpNEq -> AssocNone
  _ -> AssocLeft

formatChild :: FormatEnv -> NodePath -> Int -> Bool -> Expr -> Doc ann
formatChild env path parentPrec allowSamePrecedence expr =
  parenthesize (childPrecedence < parentPrec || (childPrecedence == parentPrec && not allowSamePrecedence)) (formatNode env path expr)
  where
    childPrecedence = exprPrec expr

formatPrefixChild :: FormatEnv -> NodePath -> Int -> Expr -> Doc ann
formatPrefixChild env path parentPrec expr =
  parenthesize (childPrec <= parentPrec) (formatNode env path expr)
  where
    childPrec = exprPrec expr

formatBinaryLeftChild :: FormatEnv -> NodePath -> BinaryOp -> Expr -> Doc ann
formatBinaryLeftChild env path op =
  formatChild env path (opPrec op) (opAssoc op == AssocLeft)

formatBinaryRightChild :: FormatEnv -> NodePath -> BinaryOp -> Expr -> Doc ann
formatBinaryRightChild env path op =
  formatChild env path (opPrec op) (opAssoc op == AssocRight)

formatNode :: FormatEnv -> NodePath -> Expr -> Doc ann
formatNode env path = \case
  NixVar _ name -> pretty name
  NixLit _ lit -> formatLit lit
  NixPar _ expr -> parens (formatTop env (childPath path 0) expr)
  NixString _ str -> formatString env path str
  NixPath _ p -> formatPath env path p
  NixEnvPath _ p -> "<" <> pretty p <> ">"
  NixLam _ pat@(NixVarPat _ _) expr -> group $ hang 2 (formatFuncPat env (childPath path 0) pat <> ":" <+> formatTop env (childPath path 1) expr)
  NixLam _ pat expr ->
    case chooseLayout env path (defaultLambdaLayout pat) of
      PreferInline -> group $ hang 2 (formatFuncPat env (childPath path 0) pat <> ":" <+> formatTop env (childPath path 1) expr)
      PreferMultiline -> formatFuncPat env (childPath path 0) pat <> ":" <> hardline <> formatTop env (childPath path 1) expr
  app@NixApp {} -> formatApp env path app
  NixBinApp _ op lhs rhs ->
    group $ hang 2 (formatBinaryLeftChild env (childPath path 0) op lhs <+> pretty (showBinOP op) <+> formatBinaryRightChild env (childPath path 1) op rhs)
  NixNotApp _ expr -> "!" <> formatPrefixChild env (childPath path 0) precNot expr
  NixNegApp _ expr -> "-" <> formatPrefixChild env (childPath path 0) precNeg expr
  NixList _ xs -> formatBracketed env path "[" "]" (zipWith (formatTop env . childPath path) [0 ..] xs)
  NixSet _ recFlag bindings -> formatSet env path recFlag bindings
  NixLet _ bindings expr -> formatLet env path bindings expr
  NixHasAttr _ expr attrPath -> formatPrefixChild env (childPath path 0) precHasAttr expr <+> "?" <+> formatAttrPath env (childPath path 1) attrPath
  NixSelect _ expr attrPath mDefault ->
    formatPrefixChild env (childPath path 0) precSelect expr <> "." <> formatAttrPath env (childPath path 1) attrPath <> maybe mempty (formatSelectDefault env (childPath path 2)) mDefault
  NixIf _ cond t f -> formatIf env path cond t f
  NixWith _ scope expr ->
    case wantsForcedMultiline expr of
      True ->
        vsep
          [ "with" <+> formatTop env (childPath path 0) scope <> ";",
            formatForcedMultiline env (childPath path 1) expr
          ]
      False -> group $ "with" <+> formatTop env (childPath path 0) scope <> ";" <+> formatTop env (childPath path 1) expr
  NixAssert _ assertion expr ->
    "assert "
      <> formatTop env (childPath path 0) assertion
      <> ";"
      <> hardline
      <> formatTop env (childPath path 1) expr

formatLit :: Lit -> Doc ann
formatLit = \case
  NixUri _ uri -> pretty uri
  NixInteger _ int -> pretty int
  NixFloat _ float -> pretty float
  NixBoolean _ bool -> if bool then "true" else "false"
  NixNull _ -> "null"

formatString :: FormatEnv -> NodePath -> NString -> Doc ann
formatString env path = \case
  NixDoubleQuotesString _ parts -> dquotes $ mconcat (zipWith (formatDoubleQuotedPart env . childPath path) [0 ..] parts)
  NixDoubleSingleQuotesString _ parts -> "''" <> mconcat (zipWith (formatIndentedPart env . childPath path) [0 ..] parts) <> "''"

formatDoubleQuotedPart :: FormatEnv -> NodePath -> StringPart -> Doc ann
formatDoubleQuotedPart env path = \case
  NixStringLiteral _ text -> pretty (escapeDoubleQuoted text)
  NixStringInterpol _ expr -> formatInterpolation env path expr

formatIndentedPart :: FormatEnv -> NodePath -> StringPart -> Doc ann
formatIndentedPart env path = \case
  NixStringLiteral _ text -> pretty (escapeIndentedText text)
  NixStringInterpol _ expr -> formatInterpolation env path expr

formatPath :: FormatEnv -> NodePath -> Path -> Doc ann
formatPath env path = \case
  NixLiteralPath _ literalPath -> pretty literalPath
  NixInterpolPath _ parts -> mconcat (zipWith (formatPathPart env . childPath path) [0 ..] parts)

formatPathPart :: FormatEnv -> NodePath -> StringPart -> Doc ann
formatPathPart env path = \case
  NixStringLiteral _ text -> pretty text
  NixStringInterpol _ expr -> formatInterpolation env path expr

formatAttrKey :: FormatEnv -> NodePath -> AttrKey -> Doc ann
formatAttrKey env path = \case
  NixStaticAttrKey _ name -> pretty name
  NixDynamicStringAttrKey _ parts -> dquotes $ mconcat (zipWith (formatDoubleQuotedPart env . childPath path) [0 ..] parts)
  NixDynamicInterpolAttrKey _ expr -> formatInterpolation env path expr

formatAttrPath :: FormatEnv -> NodePath -> AttrPath -> Doc ann
formatAttrPath env path (NixAttrPath _ keys) =
  let (prior, following) = lookupTrivia env path
      (_, prior') = splitLeadingBlankLine prior
      body = hcat $ punctuate "." (zipWith (formatAttrKey env . childPath path) [0 ..] keys)
   in applyTrivia prior' body following

formatBinding :: FormatEnv -> NodePath -> Binding -> Doc ann
formatBinding env path binding =
  case binding of
    NixNormalBinding _ attrPath expr ->
      case wantsBindingValueMultiline expr of
        True ->
          formatAttrPath env (childPath path 0) attrPath
            <+> "="
            <> hardline
            <> indent 2 (formatForcedMultiline env (childPath path 1) expr)
            <> ";"
        False -> formatAttrPath env (childPath path 0) attrPath <+> "=" <+> formatTop env (childPath path 1) expr <> ";"
    NixInheritBinding _ mScope names ->
      let (prior, following) = lookupTrivia env path
          scopeDoc = maybe mempty (\scope -> " (" <> formatInheritScope env (childPath path 0) scope <> ")") mScope
          keyDocs = zipWith (formatAttrKey env . childPath path) [0 ..] names
          namesDoc = case names of
            [] -> mempty
            _ -> " " <> fillSep keyDocs
          multiline =
            vsep
              [ "inherit" <> scopeDoc,
                indent 2 (vsep keyDocs),
                indent 2 ";"
              ]
          body = if length names >= 4 then multiline else group ("inherit" <> scopeDoc <> namesDoc <> ";")
       in applyTrivia prior body following

formatFuncPat :: FormatEnv -> NodePath -> FuncPat -> Doc ann
formatFuncPat env path = \case
  NixVarPat _ name -> pretty name
  NixSetPat _ ellipses mAs bindings ->
    case fmap nspaLocation mAs of
      Just NixSetPatAsLeading -> formatSetPatAsLeading <> formatSetPatParams
      Just NixSetPatAsTrailing -> formatSetPatParams <> formatSetPatAsTrailing
      Nothing -> formatSetPatParams
    where
      formatSetPatAsLeading = maybe mempty formatSetPatAs mAs
      formatSetPatAsTrailing = maybe mempty formatSetPatAs mAs
      entries = zipWith (formatSetPatBinding env . childPath path) [0 ..] bindings <> ["..." | ellipses == NixSetPatIsEllipses]
      formatSetPatParams =
        case chooseLayout env path (defaultSetPatLayout ellipses bindings) of
          PreferInline ->
            case (ellipses, bindings) of
              (NixSetPatNotEllipses, []) -> "{ }"
              (NixSetPatIsEllipses, []) -> "{ ... }"
              _ -> encloseSep "{ " " }" ", " entries
          PreferMultiline -> case entries of
            [] -> "{ }"
            _ ->
              let bodyEntries = fmap (<> ",") (zipWith const entries bindings)
                  ellipsisEntry = ["..." | ellipses == NixSetPatIsEllipses]
               in vsep (["{"] <> fmap indentEntry (bodyEntries <> ellipsisEntry) <> ["}"])
      indentEntry = indent 2

formatSetPatAs :: SetPatAs -> Doc ann
formatSetPatAs NixSetPatAs {..} = case nspaLocation of
  NixSetPatAsLeading -> pretty nspaVar <> "@"
  NixSetPatAsTrailing -> "@" <> pretty nspaVar

formatSetPatBinding :: FormatEnv -> NodePath -> SetPatBinding -> Doc ann
formatSetPatBinding env path NixSetPatBinding {..} = pretty nspbVar <> maybe mempty ((" ? " <>) . formatTop env (childPath path 0)) nspbDefault

defaultLambdaLayout :: FuncPat -> ContainerLayout
defaultLambdaLayout = \case
  NixVarPat {} -> PreferInline
  NixSetPat _ ellipses _ bindings -> defaultSetPatLayout ellipses bindings

defaultSetPatLayout :: NixSetPatEllipses -> [SetPatBinding] -> ContainerLayout
defaultSetPatLayout ellipses bindings =
  case (ellipses, bindings) of
    (NixSetPatNotEllipses, []) -> PreferInline
    (NixSetPatIsEllipses, []) -> PreferInline
    _ -> PreferMultiline

formatSet :: FormatEnv -> NodePath -> NixSetIsRecursive -> [Binding] -> Doc ann
formatSet env path recFlag bindings =
  case bindings of
    [] -> case recFlag of
      NixSetRecursive -> "rec { }"
      NixSetNonRecursive -> "{ }"
    _ -> case chooseLayout env path PreferMultiline of
      PreferInline -> single
      PreferMultiline -> multi
  where
    prefix = case recFlag of
      NixSetRecursive -> "rec "
      NixSetNonRecursive -> mempty
    single = prefix <> "{ " <> hsep (zipWith (formatBinding env . childPath path) [0 ..] bindings) <+> "}"
    multi =
      let bindingDocs = zipWith (formatBinding env . childPath path) [0 ..] bindings
          renderedBindings = intercalateBindingDocs bindingDocs
          openGap = if firstBindingHasLeadingBlankLine then [mempty] else []
       in vsep ([prefix <> "{"] <> openGap <> renderedBindings <> ["}"])

    firstBindingHasLeadingBlankLine =
      case bindings of
        firstBinding : _ -> bindingLeadingBlankLine env (childPath path 0) firstBinding
        [] -> False

    intercalateBindingDocs [] = []
    intercalateBindingDocs [doc] = [indent 2 doc]
    intercalateBindingDocs (doc : rest) = indent 2 doc : concatMap addBindingGap (zip [1 :: Int ..] rest)

    addBindingGap (i, doc)
      | bindingLeadingBlankLine env (childPath path i) (bindings !! i) = [mempty, indent 2 doc]
      | otherwise = [indent 2 doc]

formatLet :: FormatEnv -> NodePath -> [Binding] -> Expr -> Doc ann
formatLet env path bindings expr =
  case bindings of
    [] -> vsep ["let", "in", formatTop env (childPath path 0) expr]
    _ -> case chooseLayout env path PreferMultiline of
      PreferInline -> single
      PreferMultiline -> multi
  where
    single = "let" <+> hsep (zipWith (formatBinding env . childPath path) [0 ..] bindings) <+> "in" <+> formatTop env (childPath path (length bindings)) expr
    multi = vsep ["let", indent 2 (vsep (zipWith (formatBinding env . childPath path) [0 ..] bindings)), "in", formatTop env (childPath path (length bindings)) expr]

formatInheritScope :: FormatEnv -> NodePath -> Expr -> Doc ann
formatInheritScope env path = formatTop env path

formatApp :: FormatEnv -> NodePath -> Expr -> Doc ann
formatApp env path expr =
  case collectAppChain expr of
    [] -> mempty
    f : args ->
      let headDoc = formatAppHead env (childPath path 0) f
          argDocs = zipWith (formatAppArg env . childPath path) [1 ..] args
       in if any wantsForcedMultiline args
            then foldl (<+>) headDoc argDocs
            else group $ hang 2 (headDoc <+> hsep argDocs)

collectAppChain :: Expr -> [Expr]
collectAppChain = go []
  where
    go acc = \case
      NixApp _ f x -> go (x : acc) f
      other -> other : acc

formatAppHead :: FormatEnv -> NodePath -> Expr -> Doc ann
formatAppHead env path expr = formatChild env path precApp True expr

formatAppArg :: FormatEnv -> NodePath -> Expr -> Doc ann
formatAppArg env path expr =
  case wantsForcedMultiline expr of
    True -> parenthesizeForcedAppArg expr (formatForcedMultiline env path expr)
    False -> formatChild env path precApp False expr

parenthesizeForcedAppArg :: Expr -> Doc ann -> Doc ann
parenthesizeForcedAppArg expr doc
  | exprPrec expr <= precApp = vsep ["(", indent 2 doc, ")"]
  | otherwise = doc

formatForcedMultiline :: FormatEnv -> NodePath -> Expr -> Doc ann
formatForcedMultiline env path expr =
  case expr of
    NixSet _ recFlag bindings -> formatSet (forceMultilineEnv env path) path recFlag bindings
    NixLet _ bindings body -> formatLet (forceMultilineEnv env path) path bindings body
    _ -> formatTop env path expr

wantsForcedMultiline :: Expr -> Bool
wantsForcedMultiline = \case
  NixSet _ _ bindings -> not (null bindings)
  NixLet _ bindings _ -> not (null bindings)
  _ -> False

wantsBindingValueMultiline :: Expr -> Bool
wantsBindingValueMultiline = \case
  NixLet _ bindings _ -> not (null bindings)
  _ -> False

formatBracketed :: FormatEnv -> NodePath -> Doc ann -> Doc ann -> [Doc ann] -> Doc ann
formatBracketed env path open close docs =
  case docs of
    [] -> open <+> close
    _ -> case chooseLayout env path PreferMultiline of
      PreferInline -> single
      PreferMultiline -> multi
  where
    single = open <+> hsep docs <+> close
    multi = vsep [open, indent 2 (vsep docs), close]

formatSelectDefault :: FormatEnv -> NodePath -> Expr -> Doc ann
formatSelectDefault env path expr = " or " <> parenthesize (needsKeywordParens expr) (formatTop env path expr)

formatIf :: FormatEnv -> NodePath -> Expr -> Expr -> Expr -> Doc ann
formatIf env path cond t f =
  case f of
    NixIf _ nestedCond nestedThen nestedElse ->
      vsep
        [ "if" <+> formatTop env (childPath path 0) cond <+> "then",
          indent 2 (formatTop env (childPath path 1) t),
          formatElseIf env (childPath path 2) nestedCond nestedThen nestedElse
        ]
    _ -> group $ "if" <+> formatTop env (childPath path 0) cond <+> "then" <+> formatTop env (childPath path 1) t <+> "else" <+> formatTop env (childPath path 2) f

formatElseIf :: FormatEnv -> NodePath -> Expr -> Expr -> Expr -> Doc ann
formatElseIf env path cond t f =
  case f of
    NixIf _ nestedCond nestedThen nestedElse ->
      vsep
        [ "else if" <+> formatTop env (childPath path 0) cond <+> "then",
          indent 2 (formatTop env (childPath path 1) t),
          formatElseIf env (childPath path 2) nestedCond nestedThen nestedElse
        ]
    _ ->
      vsep
        [ "else if" <+> formatTop env (childPath path 0) cond <+> "then",
          indent 2 (formatTop env (childPath path 1) t),
          "else",
          indent 2 (formatTop env (childPath path 2) f)
        ]

needsKeywordParens :: Expr -> Bool
needsKeywordParens = \case
  NixIf {} -> True
  NixWith {} -> True
  NixAssert {} -> True
  NixLet {} -> True
  NixLam {} -> True
  NixSelect _ _ _ (Just _) -> True
  _ -> False

parenthesize :: Bool -> Doc ann -> Doc ann
parenthesize needs doc
  | needs = parens doc
  | otherwise = doc

formatCanonicalTrivia :: CanonicalTrivia -> Doc ann
formatCanonicalTrivia = \case
  CanonicalLineComment txt -> "#" <> pretty txt
  CanonicalBlockComment txt -> "/*" <> pretty txt <> "*/"
  CanonicalBlankLine -> mempty

applyTrivia :: [CanonicalTrivia] -> Doc ann -> [CanonicalTrivia] -> Doc ann
applyTrivia prior body following = prepend prior (append following body)
  where
    prepend [] doc = doc
    prepend (CanonicalBlankLine : ts) doc = hardline <> prepend ts doc
    prepend (trivia : ts) doc = formatCanonicalTrivia trivia <> hardline <> prepend ts doc

    append [] doc = doc
    append (CanonicalBlankLine : ts) doc = doc <> hardline <> append ts mempty
    append (trivia : ts) doc = doc <> hardline <> formatCanonicalTrivia trivia <> append ts mempty

splitLeadingBlankLine :: [CanonicalTrivia] -> (Bool, [CanonicalTrivia])
splitLeadingBlankLine (CanonicalBlankLine : xs) = (True, xs)
splitLeadingBlankLine xs = (False, xs)

bindingLeadingBlankLine :: FormatEnv -> NodePath -> Binding -> Bool
bindingLeadingBlankLine env path = \case
  NixNormalBinding _ _ _ ->
    case lookupTrivia env (childPath path 0) of
      (prior, _) -> fst (splitLeadingBlankLine prior)
  NixInheritBinding _ _ _ ->
    case lookupTrivia env path of
      (prior, _) -> fst (splitLeadingBlankLine prior)

forceMultilineHint :: NodePath -> LayoutHints -> LayoutHints
forceMultilineHint path hints =
  case lookupLayoutHint path hints of
    Just hint ->
      hints <> singletonLayoutHint path (LayoutHint (Just PreferMultiline) (lhPriorTrivia hint) (lhFollowingTrivia hint))
    Nothing -> hints <> singletonLayoutHint path (LayoutHint (Just PreferMultiline) [] [])

escapeDoubleQuoted :: Text -> Text
escapeDoubleQuoted = go
  where
    go text
      | T.null text = mempty
      | "${" `T.isPrefixOf` text = "\\${" <> go (T.drop 2 text)
      | otherwise =
          let (char, rest) = (T.head text, T.tail text)
           in escapeChar char <> go rest

    escapeChar char = case lookupEscape char of
      Just code -> "\\" <> T.singleton code
      Nothing -> T.singleton char

    lookupEscape char = lookup char escapes

    escapes =
      [ (decoded, encoded)
      | (encoded, decoded) <- escapedChars,
        decoded `notElem` ['{', '.']
      ]