packages feed

xml-prettify-text-1.0.0.1: src/Text/XML/Prettify.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
-- Based on Text.XML.Prettify by David M. Rosenberg
-- Copyright    : (c) 2010 David M. Rosenberg
-- License      : BSD3
-----------------------------------------------------------------------------
-- Module        :  Text.XML.Prettify
--
-- Maintained by :  Marc Jakobi, 2021-09-09
-- Modifications:
--                 - Update to Haskell 2010
--                 - Replace String with Data.Text
--                 - Encapsulate internals of module
--                 - Tweak performance
--
-- License       :  GPL2
--
-- Description   :  Pretty-print XML Text
-----------------------------------------------------------------------------
{-# LANGUAGE NoImplicitPrelude #-}

module Text.XML.Prettify
  ( XmlText,
    prettyPrintXml,
    prettyPrintXmlDefault,
    module Text.XML.Prettify.Options,
  )
where

import Control.Monad.Reader
import Data.Char (isSpace)
import qualified Data.Text as T
import Text.XML.Prettify.Options
import TextShow
import Prelude

type Prettify = Reader PrettifyOpts

-- | Pretty-print an XML text with the default options:
-- EndOfLine: LF
-- Indent style: 2 spaces
-- Returns: The pretty-printed XML
prettyPrintXmlDefault :: XmlText -> XmlText
prettyPrintXmlDefault =
  prettyPrintXml
    PrettifyOpts
      { endOfLine = LF,
        indentStyle = SPACE 2
      }

-- | Pretty-print an XML text
-- opts: The output options
-- xmlText: xml text (e.g. on one line)
-- Returnms: The pretty-printed XML
prettyPrintXml :: PrettifyOpts -> XmlText -> XmlText
prettyPrintXml opts xmlText = runReader (printAllTags tags) opts
  where
    tags = inputToTags $ T.concat $ T.lines xmlText

data TagType = IncTagType | DecTagType | StandaloneTagType
  deriving stock (Ord, Eq, Enum)

type XmlText = T.Text

type TagContent = T.Text

type OneLineXmlText = XmlText

data XmlTag = XmlTag TagContent TagType
  deriving stock (Ord, Eq)

inputToTags :: OneLineXmlText -> [XmlTag]
inputToTags "" = []
inputToTags xmlText = xtag : inputToTags xmlText'
  where
    (xtag, xmlText') = lexOne xmlText

lexOne :: OneLineXmlText -> (XmlTag, OneLineXmlText)
lexOne xmlText = case nextCharacter of
  ' ' -> (XmlTag "" StandaloneTagType, "")
  '<' -> lexOneTag xmlText
  _ -> lexNonTagged xmlText
  where
    nextWord = getWord xmlText
    nextCharacter = T.head $ nextWord <> " "

lexNonTagged :: OneLineXmlText -> (XmlTag, OneLineXmlText)
lexNonTagged xmlText = (XmlTag tagContent tagType, remaining)
  where
    nextWord = getWord xmlText
    (tagContent, remaining) = T.break (== '<') nextWord
    tagType = StandaloneTagType

getWord :: T.Text -> T.Text
getWord = T.dropWhile isSpace

lexOneTag :: OneLineXmlText -> (XmlTag, OneLineXmlText)
lexOneTag xmlText = (XmlTag tagContent tagType, res)
  where
    afterTagStart = T.dropWhile (/= '<') xmlText
    (tagContent', remaining) = T.span (/= '>') afterTagStart
    tagContent = tagContent' <> (T.singleton . T.head) remaining
    res = T.tail remaining
    tagType = case (T.index tagContent 1, T.index tagContent (T.length tagContent - 2)) of
      ('/', _) -> DecTagType
      (_, '/') -> StandaloneTagType
      ('!', _) -> StandaloneTagType
      ('?', _) -> StandaloneTagType
      (_, _) -> IncTagType

printTag :: Int -> XmlTag -> Prettify (Int, XmlText)
printTag tagIdent (XmlTag content tagType) = do
  identStyle <- asks indentStyle
  let identText = showt identStyle
  let (contentIdent, nextTagIdent) = case tagType of
        IncTagType -> (tagIdent, tagIdent + 1)
        DecTagType -> (tagIdent - 1, tagIdent - 1)
        _ -> (tagIdent, tagIdent)
  let outText = T.replicate contentIdent identText <> content
  pure (nextTagIdent, outText)

printAllTags :: [XmlTag] -> Prettify XmlText
printAllTags = printTags 0

printTags :: Int -> [XmlTag] -> Prettify XmlText
printTags _ [] = pure ""
printTags ident (tag : tags) = do
  eol <- asks endOfLine
  (nextTagIdent, tagText) <- printTag ident tag
  remainingTagText <- printTags nextTagIdent tags
  pure $ T.intercalate (showt eol) [tagText, remainingTagText]