{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE NamedFieldPuns #-}
module Xml
( Node (..)
, Content (..)
, Attribute (..)
, decode
) where
import Data.Builder.ST (Builder)
import Data.Bytes (Bytes)
import Data.Bytes.Parser (Parser)
import Data.Chunks (Chunks)
import Data.Primitive (SmallArray)
import Data.Text.Short (ShortText)
import Data.Word (Word8)
import GHC.Exts (Char (C#), Char#)
import qualified Data.Builder.ST as Builder
import qualified Data.Bytes as Bytes
import qualified Data.Bytes.Parser as Parser
import qualified Data.Bytes.Parser.Latin as Latin
import qualified Data.Bytes.Parser.Rebindable as R
import qualified Data.Bytes.Parser.Unsafe as Unsafe
import qualified Data.Bytes.Parser.Utf8 as Utf8
import qualified Data.Chunks as Chunks
import qualified Data.Text.Short as TS
import qualified Data.Text.Short.Unsafe as TS
data Node
= Text !ShortText
| Element {-# UNPACK #-} !Content
deriving (Show, Eq)
data Content = Content
{ tag :: !ShortText
, attributes :: !(SmallArray Attribute)
, children :: !(SmallArray Node)
}
deriving (Show, Eq)
data Attribute = Attribute
{ name :: !ShortText
, value :: !ShortText
}
deriving (Show, Eq)
decode :: Bytes -> Maybe Node
decode !b = Parser.parseBytesMaybe elementNodeParser b
elementNodeParser :: Parser () s Node
elementNodeParser = do
Latin.char () '<'
btag <- Parser.takeWhile (\w -> not (isXmlSpace w) && w /= 0x3E && w /= 0x2F)
case Bytes.length btag of
0 -> Parser.fail ()
_ -> pure ()
tag <- case TS.fromShortByteString (Bytes.toShortByteStringClone btag) of
Nothing -> Parser.fail ()
Just ttag -> pure ttag
-- Note that parserAttributes consumes leading and trailing whitespace.
attrs <- parserAttributes =<< Parser.effect Builder.new
let !attributes = Chunks.concat attrs
Latin.any () >>= \case
'>' -> do
nodes <- childrenParser tag
pure (Element Content {tag, attributes, children = Chunks.concat nodes})
'/' -> do
Latin.char () '>'
pure (Element Content {tag, attributes, children = mempty})
_ -> Parser.fail ()
textNodeParser :: Parser () s Node
textNodeParser = do
raw <- Parser.takeWhile (\w -> w /= 0x3C)
case Bytes.any (\w -> w > 0x7F || w == 0x26) raw of
True -> Parser.fail () -- TODO: escape or check UTF-8 encoding here instead
False -> pure (Text (TS.fromShortByteStringUnsafe (Bytes.toShortByteStringClone raw)))
-- This eats the closing tag as well.
childrenParser ::
ShortText -> -- opening tag name, looking for a closing tag that matches
Parser () s (Chunks Node)
childrenParser !tag = do
b0 <- Parser.effect Builder.new
childrenParserLoop tag b0
childrenParserLoop ::
ShortText -> -- opening tag name, looking for a closing tag that matches
Builder s Node ->
Parser () s (Chunks Node)
childrenParserLoop !tag !b0 =
Latin.any () >>= \case
'<' ->
Latin.any () >>= \case
'/' -> do
Utf8.shortText () tag
Parser.skipWhile isXmlSpace
Latin.char () '>'
Parser.effect (Builder.freeze b0)
_ -> do
Unsafe.unconsume 2
node <- elementNodeParser
b1 <- Parser.effect (Builder.push node b0)
childrenParserLoop tag b1
_ -> do
Unsafe.unconsume 1
node <- textNodeParser
b1 <- Parser.effect (Builder.push node b0)
childrenParserLoop tag b1
isXmlSpace :: Word8 -> Bool
isXmlSpace = \case
0x20 -> True
0x09 -> True
0x0D -> True
0x0A -> True
_ -> False
parserAttributes :: Builder s Attribute -> Parser () s (Chunks Attribute)
parserAttributes !b0 = do
Parser.skipWhile isXmlSpace
peekIsNameStartChar >>= \case
True -> do
attr <- parserAttribute
b1 <- Parser.effect (Builder.push attr b0)
parserAttributes b1
False -> do
Parser.skipWhile isXmlSpace
Parser.effect (Builder.freeze b0)
-- From the spec, we have:
-- Attribute ::= Name Eq AttValue
-- Eq ::= S? '=' S?
-- Name ::= NameStartChar (NameChar)*
--
-- Precondition A: The first character is a NameStartChar. This parser
-- does not check this.
parserAttribute :: Parser () s Attribute
parserAttribute = do
bname <- Parser.takeWhile (\w -> not (isXmlSpace w) && w /= 0x3D)
-- We may assume that length of bname is at least one because of
-- precondition A.
!name <- case TS.fromShortByteString (Bytes.toShortByteStringClone bname) of
Nothing -> Parser.fail ()
Just tname -> pure tname
Parser.skipWhile isXmlSpace
Latin.char () '='
Parser.skipWhile isXmlSpace
!value <- parserAttributeValue
pure Attribute {name, value}
-- TODO: This is woefully incomplete
parserAttributeValue :: Parser () s ShortText
parserAttributeValue = do
Latin.any () >>= \case
'"' -> do
bval <- Parser.takeWhile (\w -> w /= 0x22)
Latin.char () '"'
case TS.fromShortByteString (Bytes.toShortByteStringClone bval) of
Nothing -> Parser.fail ()
Just tval -> pure tval
'\'' -> do
bval <- Parser.takeWhile (\w -> w /= 0x27)
Latin.char () '\''
case TS.fromShortByteString (Bytes.toShortByteStringClone bval) of
Nothing -> Parser.fail ()
Just tval -> pure tval
_ -> Parser.fail ()
peekIsNameStartChar :: Parser () s Bool
peekIsNameStartChar =
Unsafe.cursor R.>>= \pos ->
Utf8.any# () R.>>= \c ->
Unsafe.jump pos R.>>= \_ ->
R.pure (isNameStartChar c)
isNameStartChar :: Char# -> Bool
isNameStartChar c = case C# c of
':' -> True
'_' -> True
_ | C# c >= 'A' && C# c <= 'Z' -> True
_ | C# c >= 'a' && C# c <= 'z' -> True
_ -> False