module Burrito.Expand
( expand
)
where
import qualified Burrito.Type.Expression as Expression
import qualified Burrito.Type.LitChar as LitChar
import qualified Burrito.Type.Literal as Literal
import qualified Burrito.Type.Modifier as Modifier
import qualified Burrito.Type.Name as Name
import qualified Burrito.Type.NonEmpty as NonEmpty
import qualified Burrito.Type.Operator as Operator
import qualified Burrito.Type.Template as Template
import qualified Burrito.Type.Token as Token
import qualified Burrito.Type.Value as Value
import qualified Burrito.Type.VarChar as VarChar
import qualified Burrito.Type.Variable as Variable
import qualified Data.Bits as Bits
import qualified Data.Char as Char
import qualified Data.Functor.Identity as Identity
import qualified Data.List as List
import qualified Data.Maybe as Maybe
import qualified Data.Word as Word
import qualified Text.Printf as Printf
expand :: [(String, Value.Value)] -> Template.Template -> String
expand values = Identity.runIdentity
. expandTemplate (pure . flip lookup values . expandName)
expandTemplate
:: Applicative m
=> (Name.Name -> m (Maybe Value.Value))
-> Template.Template
-> m String
expandTemplate f = expandTokens f . Template.tokens
expandTokens
:: Applicative m
=> (Name.Name -> m (Maybe Value.Value))
-> [Token.Token]
-> m String
expandTokens f = fmap concat . traverse (expandToken f)
expandToken
:: Applicative m
=> (Name.Name -> m (Maybe Value.Value))
-> Token.Token
-> m String
expandToken f token = case token of
Token.Literal literal -> pure $ expandLiteral literal
Token.Expression expression -> expandExpression f expression
expandLiteral :: Literal.Literal -> String
expandLiteral =
concatMap expandCharacter . NonEmpty.toList . Literal.characters
expandCharacter :: LitChar.LitChar -> String
expandCharacter character = case character of
LitChar.Encoded word8 -> percentEncodeWord8 word8
LitChar.Unencoded char -> escapeChar Operator.PlusSign char
escapeChar :: Operator.Operator -> Char -> String
escapeChar operator char =
if isAllowed operator char then [char] else percentEncodeChar char
isAllowed :: Operator.Operator -> Char -> Bool
isAllowed operator char = case operator of
Operator.NumberSign -> isUnreserved char || isReserved char
Operator.PlusSign -> isUnreserved char || isReserved char
_ -> isUnreserved char
percentEncodeChar :: Char -> String
percentEncodeChar = concatMap percentEncodeWord8 . encodeUtf8
percentEncodeWord8 :: Word.Word8 -> String
percentEncodeWord8 = Printf.printf "%%%02X"
expandExpression
:: Applicative m
=> (Name.Name -> m (Maybe Value.Value))
-> Expression.Expression
-> m String
expandExpression f expression =
let
operator = Expression.operator expression
prefix = prefixFor operator
separator = separatorFor operator
finalize expansions =
(if null expansions then "" else prefix)
<> List.intercalate separator expansions
in fmap finalize . expandVariables f operator $ Expression.variables
expression
prefixFor :: Operator.Operator -> String
prefixFor operator = case operator of
Operator.Ampersand -> "&"
Operator.FullStop -> "."
Operator.None -> ""
Operator.NumberSign -> "#"
Operator.PlusSign -> ""
Operator.QuestionMark -> "?"
Operator.Semicolon -> ";"
Operator.Solidus -> "/"
separatorFor :: Operator.Operator -> String
separatorFor operator = case operator of
Operator.Ampersand -> "&"
Operator.FullStop -> "."
Operator.None -> ","
Operator.NumberSign -> ","
Operator.PlusSign -> ","
Operator.QuestionMark -> "&"
Operator.Semicolon -> ";"
Operator.Solidus -> "/"
expandVariables
:: Applicative m
=> (Name.Name -> m (Maybe Value.Value))
-> Operator.Operator
-> NonEmpty.NonEmpty Variable.Variable
-> m [String]
expandVariables f operator =
fmap Maybe.catMaybes . traverse (expandVariable f operator) . NonEmpty.toList
expandVariable
:: Applicative m
=> (Name.Name -> m (Maybe Value.Value))
-> Operator.Operator
-> Variable.Variable
-> m (Maybe String)
expandVariable f operator variable =
let
name = Variable.name variable
modifier = Variable.modifier variable
in expandMaybeValue operator name modifier <$> f name
expandMaybeValue
:: Operator.Operator
-> Name.Name
-> Modifier.Modifier
-> Maybe Value.Value
-> Maybe String
expandMaybeValue operator name modifier maybeValue = do
value <- maybeValue
expandValue operator name modifier value
expandValue
:: Operator.Operator
-> Name.Name
-> Modifier.Modifier
-> Value.Value
-> Maybe String
expandValue operator name modifier value = case value of
Value.Dictionary dictionary ->
expandDictionary operator name modifier <$> NonEmpty.fromList dictionary
Value.List list ->
expandList operator name modifier <$> NonEmpty.fromList list
Value.String string -> Just $ expandString operator name modifier string
expandDictionary
:: Operator.Operator
-> Name.Name
-> Modifier.Modifier
-> NonEmpty.NonEmpty (String, String)
-> String
expandDictionary = expandElements
$ \operator _ modifier -> expandDictionaryElement operator modifier
expandDictionaryElement
:: Operator.Operator -> Modifier.Modifier -> (String, String) -> [String]
expandDictionaryElement operator modifier (name, value) =
let escape = escapeString operator Modifier.None
in
case modifier of
Modifier.Asterisk -> [escape name <> "=" <> escape value]
_ -> [escape name, escape value]
expandList
:: Operator.Operator
-> Name.Name
-> Modifier.Modifier
-> NonEmpty.NonEmpty String
-> String
expandList = expandElements $ \operator name modifier ->
pure . expandListElement operator name modifier
expandListElement
:: Operator.Operator -> Name.Name -> Modifier.Modifier -> String -> String
expandListElement operator name modifier = case modifier of
Modifier.Asterisk -> expandString operator name Modifier.None
_ -> expandString Operator.None name Modifier.None
expandElements
:: (Operator.Operator -> Name.Name -> Modifier.Modifier -> a -> [String])
-> Operator.Operator
-> Name.Name
-> Modifier.Modifier
-> NonEmpty.NonEmpty a
-> String
expandElements f operator name modifier =
let
showPrefix = case modifier of
Modifier.Asterisk -> False
_ -> case operator of
Operator.Ampersand -> True
Operator.QuestionMark -> True
Operator.Semicolon -> True
_ -> False
prefix = if showPrefix then expandName name <> "=" else ""
separator = case modifier of
Modifier.Asterisk -> separatorFor operator
_ -> ","
in
mappend prefix
. List.intercalate separator
. concatMap (f operator name modifier)
. NonEmpty.toList
expandString
:: Operator.Operator -> Name.Name -> Modifier.Modifier -> String -> String
expandString operator name modifier s =
let
prefix = case operator of
Operator.Ampersand -> expandName name <> "="
Operator.QuestionMark -> expandName name <> "="
Operator.Semicolon -> expandName name <> if null s then "" else "="
_ -> ""
in prefix <> escapeString operator modifier s
escapeString :: Operator.Operator -> Modifier.Modifier -> String -> String
escapeString operator modifier string =
concatMap (escapeChar operator) $ case modifier of
Modifier.Colon size -> take size string
_ -> string
expandName :: Name.Name -> String
expandName name = mconcat
[ expandVarChar $ Name.first name
, concatMap
(\(fullStop, varChar) ->
(if fullStop then "." else "") <> expandVarChar varChar
)
$ Name.rest name
]
expandVarChar :: VarChar.VarChar -> String
expandVarChar varChar = case varChar of
VarChar.Encoded hi lo -> ['%', hi, lo]
VarChar.Unencoded char -> [char]
encodeUtf8 :: Char -> [Word.Word8]
encodeUtf8 char =
let
oneByte x = [intToWord8 $ bitAnd 0x7f x]
twoBytes x =
[ bitOr 0xc0 . intToWord8 . bitAnd 0x3f $ bitShiftR 6 x
, bitOr 0x80 . intToWord8 $ bitAnd 0x3f x
]
threeBytes x =
[ bitOr 0xe0 . intToWord8 . bitAnd 0x0f $ bitShiftR 12 x
, bitOr 0x80 . intToWord8 . bitAnd 0x3f $ bitShiftR 6 x
, bitOr 0x80 . intToWord8 $ bitAnd 0x3f x
]
fourBytes x =
[ bitOr 0xf0 . intToWord8 . bitAnd 0x07 $ bitShiftR 18 x
, bitOr 0x80 . intToWord8 . bitAnd 0x3f $ bitShiftR 12 x
, bitOr 0x80 . intToWord8 . bitAnd 0x3f $ bitShiftR 6 x
, bitOr 0x80 . intToWord8 $ bitAnd 0x3f x
]
in case Char.ord char of
int
| int <= 0x7f -> oneByte int
| int <= 0x7ff -> twoBytes int
| int <= 0xffff -> threeBytes int
| otherwise -> fourBytes int
bitAnd :: Bits.Bits a => a -> a -> a
bitAnd = (Bits..&.)
bitOr :: Bits.Bits a => a -> a -> a
bitOr = (Bits..|.)
bitShiftR :: Bits.Bits a => Int -> a -> a
bitShiftR = flip Bits.shiftR
intToWord8 :: Int -> Word.Word8
intToWord8 x =
let
lo = word8ToInt (minBound :: Word.Word8)
hi = word8ToInt (maxBound :: Word.Word8)
in if x < lo
then error $ "intToWord8: " <> show x <> " < " <> show lo
else if x > hi
then error $ "intToWord8: " <> show x <> " > " <> show hi
else fromIntegral x
word8ToInt :: Word.Word8 -> Int
word8ToInt = fromIntegral
isAlpha :: Char -> Bool
isAlpha x = Char.isAsciiUpper x || Char.isAsciiLower x
isReserved :: Char -> Bool
isReserved x = isGenDelim x || isSubDelim x
isGenDelim :: Char -> Bool
isGenDelim x = case x of
':' -> True
'/' -> True
'?' -> True
'#' -> True
'[' -> True
']' -> True
'@' -> True
_ -> False
isSubDelim :: Char -> Bool
isSubDelim x = case x of
'!' -> True
'$' -> True
'&' -> True
'\'' -> True
'(' -> True
')' -> True
'*' -> True
'+' -> True
',' -> True
';' -> True
'=' -> True
_ -> False
isUnreserved :: Char -> Bool
isUnreserved x = case x of
'-' -> True
'.' -> True
'_' -> True
'~' -> True
_ -> isAlpha x || Char.isDigit x