packages feed

djot-0.1.0.0: src/Djot/Attributes.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Strict #-}
{-# LANGUAGE DeriveDataTypeable #-}
module Djot.Attributes
  ( pAttributes
  , parseAttributes
  , AttrParserState  -- opaque
  , AttrParseResult(..)
  )
where
import Data.Char (isAlphaNum, isSpace, isPunctuation)
import Djot.AST (Attr(..))
import Djot.Parse
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as B8
import Data.ByteString.Char8 ( (!?) )
import Data.Typeable (Typeable)
import Data.Maybe (fromMaybe)
-- import Debug.Trace


--  attributes { id = "foo", class = "bar baz",
--               key1 = "val1", key2 = "val2" }
--  syntax:
--
--  attributes <- '{' (ignorable attribute)* ignorable* '}'
--  attribute <- identifier | class | keyval
--  identifier <- '#' name
--  class <- '.' name
--  name <- (nonspace, nonpunctuation other than ':', '_', '-')+
--  keyval <- key '=' val
--  key <- (ASCII_ALPHANUM | ':' | '_' | '-')+
--  val <- bareval | quotedval
--  bareval <- (ASCII_ALPHANUM | ':' | '_' | '-')+
--  quotedval <- '"' ([^"] | '\"') '"'
--  ignorable <- whitespace | comment
--  comment <- '%' [^%}]* '%'

pAttributes :: Parser s Attr
pAttributes = lookahead (asciiChar '{') >> getSlice >>= go Nothing
 where
   getSlice = byteStringOf $
               branch
                (skipSome (skipSatisfyByte (/= '}')))
                (optional_ (asciiChar '}'))
                (asciiChar '}')
   go mbst bs = do
     case parseAttributes mbst bs of
       Done (attr, _off) -> pure attr
       Partial st -> getSlice >>= go (Just st)
       Failed _off -> failed

data AttrParseResult =
    Done (Attr, Int) -- result and byte offset
  | Failed Int -- byte offset of failure
  | Partial AttrParserState -- entire bytestring consumed
  deriving (Show, Typeable)

data AttrParserState =
  AttrParserState
  { aState :: AState
  , subject :: ByteString
  , offset :: Int
  , parts :: [AttrPart] }
  deriving (Show, Typeable)

data AState =
    SCANNING
  | AFTER_KEY
  | SCANNING_VALUE
  | SCANNING_QUOTED_VALUE
  | SCANNING_ESCAPE
  | SCANNING_COMMENT
  | FAIL
  | DONE
  | START
  deriving (Eq, Ord, Show, Typeable)

data AttrPart =
    AttrId ByteString
  | AttrClass ByteString
  | AttrKey ByteString
  | AttrValue ByteString
  deriving (Eq, Ord, Show, Typeable)

-- | Resumable parser, returning parts in reverse order.
parseAttributes :: Maybe AttrParserState -> ByteString -> AttrParseResult
parseAttributes mbState bs =
  case go (fromMaybe AttrParserState{ aState = START
                                    , subject = bs
                                    , offset = 0
                                    , parts = [] } mbState) of
    AttrParserState{ aState = DONE, parts = attparts, offset = off } ->
      Done (attrPartsToAttr attparts, off)
    AttrParserState{ aState = FAIL, offset = off } -> Failed off
    st -> Partial st
 where
  go :: AttrParserState -> AttrParserState
  go st@(AttrParserState _ subj off _) = -- trace (show st) $
     case subj !? off of
       Nothing -> st
       Just nextc ->
        case aState st of
         SCANNING ->
           case nextc of
             '}' -> go st{ aState = DONE, offset = off + 1 }
             '%' -> go st{ aState = SCANNING_COMMENT, offset = off + 1 }
             '#' -> go $ takePart isNameChar AttrId SCANNING st{ offset = off + 1 }
             '.' -> go $ takePart isNameChar AttrClass SCANNING st{ offset = off + 1 }
             c | isKeyChar c -> go $ takePart isKeyChar AttrKey AFTER_KEY st
             c | isWs c -> go $ skipWhile isWs st
             _ -> st{ aState = FAIL }
         AFTER_KEY ->
           case nextc of
             '=' -> go st{ aState = SCANNING_VALUE, offset = off + 1 }
             _ -> st{ aState = FAIL }
         SCANNING_VALUE ->
           case nextc of
             '"' -> go st{ aState = SCANNING_QUOTED_VALUE, offset = off + 1 }
             c | isBareValChar c -> go $ takePart isBareValChar AttrValue SCANNING st
             _ -> st{ aState = FAIL }
         SCANNING_QUOTED_VALUE ->
           case nextc of
             '"' -> go st{ aState = SCANNING, offset = off + 1 }
             '\\' -> go st{ aState = SCANNING_ESCAPE, offset = off + 1 }
             c | isWs c ->
                 let st' = skipWhile isWs st
                   in go st'{ parts = AttrValue " " : parts st' }
             _ -> go $ takePart (\c -> not (isWs c || c == '"' || c == '\\'))
                        AttrValue SCANNING_QUOTED_VALUE st
         SCANNING_ESCAPE ->
           go st{ aState = SCANNING_QUOTED_VALUE, offset = off + 1,
                  parts = AttrValue (B8.singleton nextc) : parts st }
         SCANNING_COMMENT ->
           case nextc of
             '%' -> go st{ aState = SCANNING, offset = off + 1 }
             '}' -> st{ aState = DONE, offset = off + 1 }
             _ -> go $ skipWhile (\c -> not (c == '%' || c == '}')) st
         FAIL -> st
         DONE -> st
         START ->
           case nextc of
             '{' -> go st{ aState = SCANNING, offset = off + 1 }
             _ -> st{ aState = FAIL }

takePart :: (Char -> Bool) -> (ByteString -> AttrPart) -> AState ->
            AttrParserState -> AttrParserState
takePart charP partConstructor nextstate st =
  case subject st !? offset st of
    Just c | charP c ->
      let val = B8.takeWhile charP (B8.drop (offset st) (subject st))
      in  st{ aState = nextstate,
              offset = offset st + B8.length val,
              parts = partConstructor val : parts st }
    _ -> st{ aState = FAIL }

skipWhile :: (Char -> Bool) -> AttrParserState -> AttrParserState
skipWhile charP st =
  case B8.findIndex (not . charP) (B8.drop (offset st) (subject st)) of
    Nothing -> st{ offset = B8.length (subject st) }
    Just i -> st{ offset = offset st + i }

attrPartsToAttr :: [AttrPart] -> Attr
attrPartsToAttr = go
 where
   go [] = Attr []
   go (AttrId bs : xs) = (<> Attr [("id",bs)]) $ go xs
   go (AttrClass bs : xs) = (<> Attr [("class",bs)]) $ go xs
   go zs =
     case break isAttrKey zs of
       (vs, AttrKey bs : xs) ->
         (<> Attr [(bs, mconcat (reverse $ map getAttrVal vs))]) $ go xs
       _ -> Attr [] -- should not happen
   isAttrKey (AttrKey _) = True
   isAttrKey _ = False
   getAttrVal (AttrValue bs) = bs
   getAttrVal _ = mempty

isNameChar :: Char -> Bool
isNameChar c = not (isSpace c)
    && (not (isPunctuation c) || c == ':' || c == '_' || c == '-')

isKeyChar :: Char -> Bool
isKeyChar c = isAlphaNum c || c == ':' || c == '_' || c == '-'

isBareValChar :: Char -> Bool
isBareValChar c = isAlphaNum c || c == ':' || c == '_' || c == '-'