module Old.Hamlet ( parse' , render' ) where import Control.Applicative ((<$>), Applicative (..)) import Control.Monad import Control.Arrow import Data.Data import Data.List (intercalate) import Text.ParserCombinators.Parsec hiding (Line) import Text.Shakespeare (Deref (..), Ident (..)) import Text.Hamlet.Parse (Line (..), Content (..)) import Old.Utf8 (renderDeref) renderAttr (mderef, name, val) = concat [ " " , case mderef of Nothing -> "" Just deref -> concat [ ":" , renderDeref deref , ":" ] , name , "=\"" , concatMap renderContent val , "\"" ] renderClass c = ' ': '.' : concatMap renderContent c renderContent (ContentRaw s) = s renderContent (ContentVar d) = concat ["#{", renderDeref d, "}"] renderContent (ContentUrl False d) = concat ["@{", renderDeref d, "}"] renderContent (ContentUrl True d) = concat ["@?{", renderDeref d, "}"] renderContent (ContentEmbed d) = concat ["^{", renderDeref d, "}"] renderLine' (indent, x) = concat [replicate indent ' ', renderLine x, "\n"] renderLine (LineForall deref (Ident i)) = concat [ "$forall " , i , " <- " , renderDeref deref ] renderLine (LineIf deref) = concat [ "$if " , renderDeref deref ] renderLine (LineElseIf deref) = concat [ "$elseif " , renderDeref deref ] renderLine LineElse = "$else" renderLine (LineMaybe deref (Ident i)) = concat [ "$maybe " , i , " <- " , renderDeref deref ] renderLine LineNothing = "$nothing" renderLine (LineTag tn attrs content classes) = concat [ "<" , tn , concatMap renderAttr attrs , concatMap renderClass classes , ">" , concatMap renderContent content ] renderLine (LineContent c) = '\\' : concatMap renderContent c data Result v = Error String | Ok v instance Monad Result where return = Ok Error s >>= _ = Error s Ok v >>= f = f v fail = Error instance Functor Result where fmap = liftM instance Applicative Result where pure = return (<*>) = ap parseLines :: HamletSettings -> String -> Result [(Int, Line)] parseLines set s = case parse (many $ parseLine set) s s of Left e -> Error $ show e Right x -> Ok x parseLine :: HamletSettings -> Parser (Int, Line) parseLine set = do ss <- fmap sum $ many ((char ' ' >> return 1) <|> (char '\t' >> return 4)) x <- doctype <|> comment <|> backslash <|> controlIf <|> controlElseIf <|> (try (string "$else") >> many (oneOf " \t") >> eol >> return LineElse) <|> controlMaybe <|> (try (string "$nothing") >> many (oneOf " \t") >> eol >> return LineNothing) <|> controlForall <|> tag <|> (eol' >> return (LineContent [])) <|> (do cs <- content InContent isEof <- (eof >> return True) <|> return False if null cs && ss == 0 && isEof then fail "End of Hamlet template" else return $ LineContent cs) return (ss, x) where eol' = (char '\n' >> return ()) <|> (string "\r\n" >> return ()) eol = eof <|> eol' doctype = do try $ string "!!!" >> eol return $ LineContent [ContentRaw $ hamletDoctype set ++ "\n"] comment = do _ <- try $ string "$#" _ <- many $ noneOf "\r\n" eol return $ LineContent [] backslash = do _ <- char '\\' (eol >> return (LineContent [ContentRaw "\n"])) <|> (LineContent <$> content InContent) controlIf = do _ <- try $ string "$if" spaces x <- deref False _ <- many $ oneOf " \t" eol return $ LineIf x controlElseIf = do _ <- try $ string "$elseif" spaces x <- deref False _ <- many $ oneOf " \t" eol return $ LineElseIf x controlMaybe = do _ <- try $ string "$maybe" spaces x <- deref False spaces y <- ident _ <- many $ oneOf " \t" eol return $ LineMaybe x y controlForall = do _ <- try $ string "$forall" spaces x <- deref False spaces y <- ident _ <- many $ oneOf " \t" eol return $ LineForall x y tag = do x <- tagName <|> tagIdent <|> tagClass <|> tagAttrib xs <- many $ tagIdent <|> tagClass <|> tagAttrib c <- (eol >> return []) <|> (do _ <- many1 $ oneOf " \t" content InContent) let (tn, attr, classes) = tag' $ x : xs return $ LineTag tn attr c classes content cr = do x <- many $ content' cr case cr of InQuotes -> char '"' >> return () NotInQuotes -> return () InContent -> (char '$' >> eol) <|> eol return x content' cr = try contentDollar <|> contentAt <|> contentCarrot <|> contentReg cr contentDollar = do _ <- char '$' (char '$' >> return (ContentRaw "$")) <|> (do s <- deref True _ <- char '$' return $ ContentVar s) contentAt = do _ <- char '@' (char '@' >> return (ContentRaw "@")) <|> (do x <- (char '?' >> return True) <|> return False s <- deref True _ <- char '@' return $ ContentUrl x s) contentCarrot = do _ <- char '^' (char '^' >> return (ContentRaw "^")) <|> (do s <- deref True _ <- char '^' return $ ContentEmbed s) contentReg InContent = ContentRaw <$> many1 (noneOf "$@^\r\n") contentReg NotInQuotes = ContentRaw <$> many1 (noneOf "$@^#.! \t\n\r") contentReg InQuotes = (do _ <- char '\\' ContentRaw . return <$> anyChar ) <|> (ContentRaw <$> many1 (noneOf "$@^\\\"\n\r")) tagName = do _ <- char '%' s <- many1 $ noneOf " \t.#!\r\n" return $ TagName s tagAttribValue = do cr <- (char '"' >> return InQuotes) <|> return NotInQuotes content cr tagIdent = char '#' >> TagIdent <$> tagAttribValue tagClass = char '.' >> TagClass <$> tagAttribValue tagAttrib = do _ <- char '!' cond <- (Just <$> tagAttribCond) <|> return Nothing s <- many1 $ noneOf " \t.!=\r\n" v <- (do _ <- char '=' s' <- tagAttribValue return s') <|> return [] return $ TagAttrib (cond, s, v) tagAttribCond = do _ <- char ':' d <- deref True _ <- char ':' return d tag' = foldr tag'' ("div", [], []) tag'' (TagName s) (_, y, z) = (s, y, z) tag'' (TagIdent s) (x, y, z) = (x, (Nothing, "id", s) : y, z) tag'' (TagClass s) (x, y, z) = (x, y, s : z) tag'' (TagAttrib s) (x, y, z) = (x, s : y, z) derefParens = between (char '(') (char ')') $ deref True derefSingle = derefParens <|> fmap DerefIdent ident deref spaceAllowed = do let delim = if spaceAllowed then (char '.' <|> (many1 (char ' ') >> return ' ')) else char '.' x <- derefSingle xs <- many $ delim >> derefSingle return $ foldr1 DerefBranch $ x : xs ident = Ident <$> many1 (alphaNum <|> char '_' <|> char '\'') data TagPiece = TagName String | TagIdent [Content] | TagClass [Content] | TagAttrib (Maybe Deref, String, [Content]) data ContentRule = InQuotes | NotInQuotes | InContent data Nest = Nest Line [Nest] nestLines :: [(Int, Line)] -> [Nest] nestLines [] = [] nestLines ((i, l):rest) = let (deeper, rest') = span (\(i', _) -> i' > i) rest in Nest l (nestLines deeper) : nestLines rest' -- | Settings for parsing of a hamlet document. data HamletSettings = HamletSettings { -- | The value to replace a \"!!!\" with. Do not include the trailing -- newline. hamletDoctype :: String -- | 'True' means to close empty tags (eg, img) with a trailing slash, ie -- XML-style empty tags. 'False' uses HTML-style. , hamletCloseEmpties :: Bool -- | Should we put a newline after closing a tag? , hamletCloseNewline :: Bool } -- | Defaults settings: HTML5 doctype and HTML-style empty tags. defaultHamletSettings :: HamletSettings defaultHamletSettings = HamletSettings "" False False xhtmlHamletSettings :: HamletSettings xhtmlHamletSettings = HamletSettings doctype True False where doctype = "" debugHamletSettings :: HamletSettings debugHamletSettings = HamletSettings "" False True data CloseStyle = NoClose | CloseInside | CloseSeparate -- FIXME A breaking change, but move closeTag to be a record in the -- HamletSettings datatype. Would allow more precise XML encodings. closeTag :: HamletSettings -> String -> CloseStyle closeTag h s = if canBeEmpty s then CloseSeparate else (if hamletCloseEmpties h then CloseInside else NoClose) where canBeEmpty "img" = False canBeEmpty "link" = False canBeEmpty "meta" = False canBeEmpty "br" = False canBeEmpty "hr" = False canBeEmpty "input" = False canBeEmpty _ = True parse' set s = case parseLines defaultHamletSettings s of Error e -> error e Ok x -> x render' = concatMap renderLine'