From e05de3273b31f3561a772e2857885bcc426da558 Mon Sep 17 00:00:00 2001 From: lillo Date: Sun, 27 Oct 2024 02:25:45 +0700 Subject: [PATCH 01/29] add new module --- ihp-hsx/IHP/HSX/UncheckedHSX.hs | 115 ++++++++++++++++++++++++++++++++ ihp-hsx/ihp-hsx.cabal | 3 +- 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 ihp-hsx/IHP/HSX/UncheckedHSX.hs diff --git a/ihp-hsx/IHP/HSX/UncheckedHSX.hs b/ihp-hsx/IHP/HSX/UncheckedHSX.hs new file mode 100644 index 000000000..dd0c731ae --- /dev/null +++ b/ihp-hsx/IHP/HSX/UncheckedHSX.hs @@ -0,0 +1,115 @@ +{-# LANGUAGE TemplateHaskell, QuasiQuotes, OverloadedStrings #-} +module IHP.HSX.UncheckedHSX (uncheckedHsx) where + +import Prelude +import Language.Haskell.TH +import Language.Haskell.TH.Quote +import Text.Blaze.Html5 ((!)) +import qualified Text.Blaze.Html5 as H +import qualified Text.Blaze.Html5.Attributes as A +import Data.Text (Text) +import qualified Data.Text as T +import Text.Megaparsec +import Text.Megaparsec.Char +import Data.Void +import Data.String.Conversions (cs) +import qualified Data.Set as Set + +uncheckedHsx :: QuasiQuoter +uncheckedHsx = QuasiQuoter + { quoteExp = quoteUncheckedHsxExpression + , quotePat = error "quotePat not implemented for uncheckedHsx" + , quoteType = error "quoteType not implemented for uncheckedHsx" + , quoteDec = error "quoteDec not implemented for uncheckedHsx" + } + +quoteUncheckedHsxExpression :: String -> Q Exp +quoteUncheckedHsxExpression code = do + loc <- location + let position = SourcePos (loc_filename loc) (mkPos (fst (loc_start loc))) (mkPos (snd (loc_start loc))) + case runParser uncheckedParser "" (cs code) of + Left err -> fail $ errorBundlePretty err + Right result -> compileToHaskell result + +type Parser = Parsec Void Text + +data UNode = UNode Text [(Text, Text)] [UNode] Bool + | UTextNode Text + | USplicedNode Exp + deriving (Show) + +uncheckedParser :: Parser UNode +uncheckedParser = space *> (manyUncheckedElement <|> uncheckedElement) <* space <* eof + +manyUncheckedElement :: Parser UNode +manyUncheckedElement = do + children <- many uncheckedChild + return $ UNode "div" [] children False + +uncheckedElement :: Parser UNode +uncheckedElement = do + char '<' + tagName <- some (alphaNumChar <|> char '-' <|> char '_') + attrs <- many uncheckedAttribute + space + (do string "/>" + return $ UNode (T.pack tagName) attrs [] True) + <|> + (do char '>' + children <- many uncheckedChild + string "' + return $ UNode (T.pack tagName) attrs children False) + <|> + (do char '>' + if tagName `Set.member` selfClosingTags + then return $ UNode (T.pack tagName) attrs [] True + else do + children <- many uncheckedChild + string "' + return $ UNode (T.pack tagName) attrs children False) + +uncheckedAttribute :: Parser (Text, Text) +uncheckedAttribute = do + space + name <- some (alphaNumChar <|> char '-' <|> char '_') + value <- option "" (char '=' *> (quoted <|> unquoted)) + return (T.pack name, T.pack value) + where + quoted = char '"' *> manyTill anySingle (char '"') + unquoted = some (alphaNumChar <|> char '-' <|> char '_') + +uncheckedChild :: Parser UNode +uncheckedChild = uncheckedElement <|> uncheckedTextNode <|> uncheckedSplicedNode + +uncheckedTextNode :: Parser UNode +uncheckedTextNode = UTextNode . T.pack <$> some (anySingleBut '<') + +uncheckedSplicedNode :: Parser UNode +uncheckedSplicedNode = between (string "{") (string "}") $ do + expr <- parseHaskellExpression + return $ USplicedNode expr + +parseHaskellExpression :: Parser Exp +parseHaskellExpression = error "Implement Haskell expression parsing here" + +compileToHaskell :: UNode -> Q Exp +compileToHaskell (UNode name attrs children isLeaf) = + let element = if isLeaf + then [| H.preEscapedText $(litE $ stringL $ T.unpack $ "<" <> name <> "/>") |] + else [| H.preEscapedText $(litE $ stringL $ T.unpack $ "<" <> name <> ">") |] + applyAttrs = foldr (\(k, v) e -> [| $e ! H.customAttribute (H.stringTag $(litE $ stringL $ T.unpack k)) $(litE $ stringL $ T.unpack v) |]) element attrs + applyChildren = if null children + then applyAttrs + else [| $applyAttrs <> mconcat $(listE (map compileToHaskell children)) <> H.preEscapedText $(litE $ stringL $ T.unpack $ " name <> ">") |] + in applyChildren +compileToHaskell (UTextNode text) = [| H.text $(litE $ stringL $ T.unpack text) |] +compileToHaskell (USplicedNode exp) = [| H.preEscapedToHtml $(return exp) |] + +selfClosingTags :: Set.Set String +selfClosingTags = Set.fromList ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"] + +-- You can add more helper functions here as needed diff --git a/ihp-hsx/ihp-hsx.cabal b/ihp-hsx/ihp-hsx.cabal index b6973912d..42df73cea 100644 --- a/ihp-hsx/ihp-hsx.cabal +++ b/ihp-hsx/ihp-hsx.cabal @@ -1,6 +1,6 @@ cabal-version: 2.2 name: ihp-hsx -version: 1.3.0 +version: 1.4.0 synopsis: JSX-like but for Haskell description: JSX-like templating syntax for Haskell license: MIT @@ -89,6 +89,7 @@ library , IHP.HSX.HaskellParser , IHP.HSX.HsExpToTH , IHP.HSX.Attribute + , IHP.HSX.UncheckedHSX test-suite ihp-hsx-tests type: exitcode-stdio-1.0 From 595f7780e5fc07f0540390f2bdfacce4ca16dc76 Mon Sep 17 00:00:00 2001 From: lillo Date: Sun, 27 Oct 2024 04:00:01 +0700 Subject: [PATCH 02/29] Fix uncheckedHSX quasiquoter --- ihp-hsx/IHP/HSX/QQ.hs | 4 +- ihp-hsx/IHP/HSX/UncheckedHSX.hs | 102 ++++++++++++++++---------------- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/ihp-hsx/IHP/HSX/QQ.hs b/ihp-hsx/IHP/HSX/QQ.hs index a064f2fb4..7b4cf7ddf 100644 --- a/ihp-hsx/IHP/HSX/QQ.hs +++ b/ihp-hsx/IHP/HSX/QQ.hs @@ -5,7 +5,7 @@ Module: IHP.HSX.QQ Description: Defines the @[hsx||]@ syntax Copyright: (c) digitally induced GmbH, 2022 -} -module IHP.HSX.QQ (hsx) where +module IHP.HSX.QQ (hsx, uncheckedHsx) where import Prelude import Data.Text (Text) @@ -27,6 +27,7 @@ import Data.List (foldl') import IHP.HSX.Attribute import qualified Text.Blaze.Html5.Attributes as Attributes import qualified Data.HashMap.Strict as HashMap +import IHP.HSX.UncheckedHSX (uncheckedHsx) hsx :: QuasiQuoter hsx = QuasiQuoter { @@ -459,3 +460,4 @@ textToStaticString text = StaticString (Text.unpack text ++) (Text.encodeUtf8 te instance Show (MarkupM ()) where show html = BlazeString.renderHtml html + diff --git a/ihp-hsx/IHP/HSX/UncheckedHSX.hs b/ihp-hsx/IHP/HSX/UncheckedHSX.hs index dd0c731ae..f8c91ca0e 100644 --- a/ihp-hsx/IHP/HSX/UncheckedHSX.hs +++ b/ihp-hsx/IHP/HSX/UncheckedHSX.hs @@ -13,7 +13,18 @@ import Text.Megaparsec import Text.Megaparsec.Char import Data.Void import Data.String.Conversions (cs) -import qualified Data.Set as Set +import qualified IHP.HSX.HaskellParser as HaskellParser +import Text.Blaze.Internal (MarkupM(Parent, Leaf), attribute) +import qualified Data.List.NonEmpty as NE + +type Parser = Parsec Void Text + +data UNode = UNode Text [(Text, AttributeValue)] [UNode] Bool + | UTextNode Text + | USplicedNode Exp + deriving (Show) + +data AttributeValue = TextValue Text | ExpressionValue Exp deriving (Show) uncheckedHsx :: QuasiQuoter uncheckedHsx = QuasiQuoter @@ -27,89 +38,76 @@ quoteUncheckedHsxExpression :: String -> Q Exp quoteUncheckedHsxExpression code = do loc <- location let position = SourcePos (loc_filename loc) (mkPos (fst (loc_start loc))) (mkPos (snd (loc_start loc))) - case runParser uncheckedParser "" (cs code) of + extensions <- extsEnabled + case runParser (uncheckedParser extensions) "" (cs code) of Left err -> fail $ errorBundlePretty err Right result -> compileToHaskell result -type Parser = Parsec Void Text - -data UNode = UNode Text [(Text, Text)] [UNode] Bool - | UTextNode Text - | USplicedNode Exp - deriving (Show) - -uncheckedParser :: Parser UNode -uncheckedParser = space *> (manyUncheckedElement <|> uncheckedElement) <* space <* eof +uncheckedParser :: [Extension] -> Parser UNode +uncheckedParser extensions = space *> manyUncheckedElement <* eof manyUncheckedElement :: Parser UNode manyUncheckedElement = do - children <- many uncheckedChild + children <- many (try uncheckedElement <|> uncheckedTextNode <|> uncheckedSplicedNode) return $ UNode "div" [] children False uncheckedElement :: Parser UNode uncheckedElement = do char '<' - tagName <- some (alphaNumChar <|> char '-' <|> char '_') + tagName <- some (alphaNumChar <|> char '-' <|> char '_' <|> char ':') attrs <- many uncheckedAttribute space (do string "/>" return $ UNode (T.pack tagName) attrs [] True) <|> (do char '>' - children <- many uncheckedChild - string "' - return $ UNode (T.pack tagName) attrs children False) - <|> - (do char '>' - if tagName `Set.member` selfClosingTags - then return $ UNode (T.pack tagName) attrs [] True - else do - children <- many uncheckedChild - string "' - return $ UNode (T.pack tagName) attrs children False) + children <- many (try uncheckedElement <|> uncheckedTextNode <|> uncheckedSplicedNode) + closing <- optional (try $ string "> string (T.pack tagName) >> char '>') + case closing of + Just _ -> return $ UNode (T.pack tagName) attrs children False + Nothing -> return $ UNode (T.pack tagName) attrs children True) -uncheckedAttribute :: Parser (Text, Text) +uncheckedAttribute :: Parser (Text, AttributeValue) uncheckedAttribute = do space - name <- some (alphaNumChar <|> char '-' <|> char '_') - value <- option "" (char '=' *> (quoted <|> unquoted)) - return (T.pack name, T.pack value) + name <- some (alphaNumChar <|> char '-' <|> char '_' <|> char ':') + value <- option (TextValue "") (char '=' *> (quotedValue <|> unquotedValue <|> expressionValue)) + return (T.pack name, value) where - quoted = char '"' *> manyTill anySingle (char '"') - unquoted = some (alphaNumChar <|> char '-' <|> char '_') - -uncheckedChild :: Parser UNode -uncheckedChild = uncheckedElement <|> uncheckedTextNode <|> uncheckedSplicedNode + quotedValue = TextValue . T.pack <$> (char '"' *> manyTill anySingle (char '"')) + unquotedValue = TextValue . T.pack <$> some (alphaNumChar <|> char '-' <|> char '_') + expressionValue = ExpressionValue <$> (char '{' *> parseHaskellExpression <* char '}') uncheckedTextNode :: Parser UNode -uncheckedTextNode = UTextNode . T.pack <$> some (anySingleBut '<') +uncheckedTextNode = UTextNode . T.pack <$> some (satisfy (\c -> c /= '<' && c /= '{' && c /= '}')) uncheckedSplicedNode :: Parser UNode -uncheckedSplicedNode = between (string "{") (string "}") $ do - expr <- parseHaskellExpression - return $ USplicedNode expr +uncheckedSplicedNode = USplicedNode <$> (char '{' *> parseHaskellExpression <* char '}') parseHaskellExpression :: Parser Exp -parseHaskellExpression = error "Implement Haskell expression parsing here" +parseHaskellExpression = do + pos <- getSourcePos + code <- takeWhileP Nothing (\c -> c /= '}') + case HaskellParser.parseHaskellExpression pos [] (cs code) of + Right exp -> return exp + Left err -> fail $ show err compileToHaskell :: UNode -> Q Exp compileToHaskell (UNode name attrs children isLeaf) = - let element = if isLeaf - then [| H.preEscapedText $(litE $ stringL $ T.unpack $ "<" <> name <> "/>") |] - else [| H.preEscapedText $(litE $ stringL $ T.unpack $ "<" <> name <> ">") |] - applyAttrs = foldr (\(k, v) e -> [| $e ! H.customAttribute (H.stringTag $(litE $ stringL $ T.unpack k)) $(litE $ stringL $ T.unpack v) |]) element attrs + let element = [| H.preEscapedText $(litE $ stringL $ "<" ++ T.unpack name) |] + applyAttrs = foldr (\(k, v) e -> [| $e <> $(compileAttribute k v) |]) element attrs + closeTag = if isLeaf + then [| H.preEscapedText $(litE $ stringL "/>") |] + else [| H.preEscapedText $(litE $ stringL ">") |] applyChildren = if null children - then applyAttrs - else [| $applyAttrs <> mconcat $(listE (map compileToHaskell children)) <> H.preEscapedText $(litE $ stringL $ T.unpack $ " name <> ">") |] + then [| $applyAttrs <> $closeTag |] + else [| $applyAttrs <> H.preEscapedText $(litE $ stringL ">") <> + mconcat $(listE (map compileToHaskell children)) <> + H.preEscapedText $(litE $ stringL $ "") |] in applyChildren compileToHaskell (UTextNode text) = [| H.text $(litE $ stringL $ T.unpack text) |] compileToHaskell (USplicedNode exp) = [| H.preEscapedToHtml $(return exp) |] -selfClosingTags :: Set.Set String -selfClosingTags = Set.fromList ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"] - --- You can add more helper functions here as needed +compileAttribute :: Text -> AttributeValue -> Q Exp +compileAttribute name (TextValue value) = [| H.preEscapedText $(litE $ stringL $ " " ++ T.unpack name ++ "=\"" ++ T.unpack value ++ "\"") |] +compileAttribute name (ExpressionValue exp) = [| H.preEscapedText (T.pack $ " " ++ T.unpack name ++ "=\"") <> H.toHtml $(return exp) <> H.preEscapedText "\"" |] From 0644e5b386d8d924e1844fa4e877bcfbc7a9483a Mon Sep 17 00:00:00 2001 From: lillo Date: Sun, 27 Oct 2024 04:40:50 +0700 Subject: [PATCH 03/29] Make sure closing tags are checked --- ihp-hsx/IHP/HSX/UncheckedHSX.hs | 97 ++++++++++++++++++++++++--------- 1 file changed, 71 insertions(+), 26 deletions(-) diff --git a/ihp-hsx/IHP/HSX/UncheckedHSX.hs b/ihp-hsx/IHP/HSX/UncheckedHSX.hs index f8c91ca0e..ba1bdd081 100644 --- a/ihp-hsx/IHP/HSX/UncheckedHSX.hs +++ b/ihp-hsx/IHP/HSX/UncheckedHSX.hs @@ -16,12 +16,20 @@ import Data.String.Conversions (cs) import qualified IHP.HSX.HaskellParser as HaskellParser import Text.Blaze.Internal (MarkupM(Parent, Leaf), attribute) import qualified Data.List.NonEmpty as NE +import qualified Data.Set as Set +import Control.Monad (when) +import Data.Char (isLower) +import qualified Text.Megaparsec.Char.Lexer as L +import Data.Functor (void) +import qualified Text.Megaparsec.Char as C type Parser = Parsec Void Text data UNode = UNode Text [(Text, AttributeValue)] [UNode] Bool | UTextNode Text | USplicedNode Exp + | UCommentNode Text + | UNoRenderCommentNode deriving (Show) data AttributeValue = TextValue Text | ExpressionValue Exp deriving (Show) @@ -49,32 +57,34 @@ uncheckedParser extensions = space *> manyUncheckedElement <* eof manyUncheckedElement :: Parser UNode manyUncheckedElement = do children <- many (try uncheckedElement <|> uncheckedTextNode <|> uncheckedSplicedNode) - return $ UNode "div" [] children False + case children of + [node] -> return node + _ -> return $ UNode "div" [] children False uncheckedElement :: Parser UNode uncheckedElement = do - char '<' - tagName <- some (alphaNumChar <|> char '-' <|> char '_' <|> char ':') + void $ char '<' + tagName <- T.pack <$> some (alphaNumChar <|> char '-' <|> char '_' <|> char ':') attrs <- many uncheckedAttribute space - (do string "/>" - return $ UNode (T.pack tagName) attrs [] True) - <|> - (do char '>' - children <- many (try uncheckedElement <|> uncheckedTextNode <|> uncheckedSplicedNode) - closing <- optional (try $ string "> string (T.pack tagName) >> char '>') - case closing of - Just _ -> return $ UNode (T.pack tagName) attrs children False - Nothing -> return $ UNode (T.pack tagName) attrs children True) + if Set.member tagName selfClosingTags + then (void (string "/>" <|> string ">")) >> return (UNode tagName attrs [] True) + else do + void $ char '>' + children <- many (try uncheckedElement <|> uncheckedTextNode <|> uncheckedSplicedNode) + closing <- optional (try $ string " chunk tagName *> char '>') + case closing of + Just _ -> return $ UNode tagName attrs children False + Nothing -> fail $ "Unclosed tag: <" ++ T.unpack tagName ++ ">" uncheckedAttribute :: Parser (Text, AttributeValue) uncheckedAttribute = do space - name <- some (alphaNumChar <|> char '-' <|> char '_' <|> char ':') + name <- T.pack <$> some (alphaNumChar <|> char '-' <|> char '_' <|> char ':') value <- option (TextValue "") (char '=' *> (quotedValue <|> unquotedValue <|> expressionValue)) - return (T.pack name, value) + return (name, value) where - quotedValue = TextValue . T.pack <$> (char '"' *> manyTill anySingle (char '"')) + quotedValue = TextValue . T.pack <$> (char '"' *> manyTill L.charLiteral (char '"')) unquotedValue = TextValue . T.pack <$> some (alphaNumChar <|> char '-' <|> char '_') expressionValue = ExpressionValue <$> (char '{' *> parseHaskellExpression <* char '}') @@ -88,25 +98,60 @@ parseHaskellExpression :: Parser Exp parseHaskellExpression = do pos <- getSourcePos code <- takeWhileP Nothing (\c -> c /= '}') - case HaskellParser.parseHaskellExpression pos [] (cs code) of + case HaskellParser.parseHaskellExpression pos [] (T.unpack code) of Right exp -> return exp Left err -> fail $ show err compileToHaskell :: UNode -> Q Exp -compileToHaskell (UNode name attrs children isLeaf) = - let element = [| H.preEscapedText $(litE $ stringL $ "<" ++ T.unpack name) |] +compileToHaskell (UNode name attrs children isLeaf) = do + when (T.toLower name `notElem` knownElements && T.toLower name `notElem` knownLeafs) $ + case validateCustomElement name of + Left err -> fail err + Right () -> pure () + when (not isLeaf && null children && name `notElem` voidElements) $ + fail $ "Empty non-void element: <" ++ T.unpack name ++ ">" + let element = if isLeaf || name `elem` voidElements + then nodeToBlazeLeaf name + else nodeToBlazeElement name applyAttrs = foldr (\(k, v) e -> [| $e <> $(compileAttribute k v) |]) element attrs closeTag = if isLeaf - then [| H.preEscapedText $(litE $ stringL "/>") |] - else [| H.preEscapedText $(litE $ stringL ">") |] + then [| mempty |] + else [| H.preEscapedText $(litE $ stringL $ "") |] applyChildren = if null children - then [| $applyAttrs <> $closeTag |] - else [| $applyAttrs <> H.preEscapedText $(litE $ stringL ">") <> - mconcat $(listE (map compileToHaskell children)) <> - H.preEscapedText $(litE $ stringL $ "") |] - in applyChildren -compileToHaskell (UTextNode text) = [| H.text $(litE $ stringL $ T.unpack text) |] + then [| $applyAttrs |] + else [| $applyAttrs <> mconcat $(listE (map compileToHaskell children)) |] + [| $applyChildren <> $closeTag |] +compileToHaskell (UTextNode value) = [| H.text $(litE $ stringL $ T.unpack value) |] compileToHaskell (USplicedNode exp) = [| H.preEscapedToHtml $(return exp) |] +compileToHaskell (UCommentNode value) = [| H.textComment $(litE $ stringL $ T.unpack value) |] +compileToHaskell UNoRenderCommentNode = [| mempty |] + +selfClosingTags :: Set.Set Text +selfClosingTags = Set.fromList + [ "area", "base", "br", "col", "embed", "hr", "img", "input" + , "link", "meta", "param", "source", "track", "wbr" + ] + +voidElements :: [Text] +voidElements = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"] + +validateCustomElement :: Text -> Either String () +validateCustomElement name + | T.any (== '-') name && not (isLower (T.head name)) = Left $ "Custom element '" ++ T.unpack name ++ "' must start with a lowercase letter" + | T.any (== '-') name = Right () + | otherwise = Left $ "Custom element '" ++ T.unpack name ++ "' must contain a hyphen (-) and start with a lowercase letter" + +knownElements :: [Text] +knownElements = ["div", "span", "p", "a", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "table", "tr", "td", "th", "form", "input", "button", "select", "option", "textarea", "label", "header", "footer", "nav", "main", "section", "article", "aside"] + +knownLeafs :: [Text] +knownLeafs = ["br", "hr", "img", "input", "meta", "link"] + +nodeToBlazeLeaf :: Text -> Q Exp +nodeToBlazeLeaf name = [| H.preEscapedText $(litE $ stringL $ "<" ++ T.unpack name ++ "/>") |] + +nodeToBlazeElement :: Text -> Q Exp +nodeToBlazeElement name = [| H.preEscapedText $(litE $ stringL $ "<" ++ T.unpack name ++ ">") |] compileAttribute :: Text -> AttributeValue -> Q Exp compileAttribute name (TextValue value) = [| H.preEscapedText $(litE $ stringL $ " " ++ T.unpack name ++ "=\"" ++ T.unpack value ++ "\"") |] From 4a3553ab7205653b7acc4ba3fb2d1b48c24874d3 Mon Sep 17 00:00:00 2001 From: lillo Date: Sun, 27 Oct 2024 05:12:11 +0700 Subject: [PATCH 04/29] Fix it so it works --- ihp-hsx/IHP/HSX/UncheckedHSX.hs | 86 +++++++++------------------------ 1 file changed, 24 insertions(+), 62 deletions(-) diff --git a/ihp-hsx/IHP/HSX/UncheckedHSX.hs b/ihp-hsx/IHP/HSX/UncheckedHSX.hs index ba1bdd081..c929433fb 100644 --- a/ihp-hsx/IHP/HSX/UncheckedHSX.hs +++ b/ihp-hsx/IHP/HSX/UncheckedHSX.hs @@ -16,20 +16,15 @@ import Data.String.Conversions (cs) import qualified IHP.HSX.HaskellParser as HaskellParser import Text.Blaze.Internal (MarkupM(Parent, Leaf), attribute) import qualified Data.List.NonEmpty as NE -import qualified Data.Set as Set -import Control.Monad (when) -import Data.Char (isLower) -import qualified Text.Megaparsec.Char.Lexer as L import Data.Functor (void) -import qualified Text.Megaparsec.Char as C +import Data.Set (Set) +import qualified Data.Set as Set type Parser = Parsec Void Text data UNode = UNode Text [(Text, AttributeValue)] [UNode] Bool | UTextNode Text | USplicedNode Exp - | UCommentNode Text - | UNoRenderCommentNode deriving (Show) data AttributeValue = TextValue Text | ExpressionValue Exp deriving (Show) @@ -57,9 +52,7 @@ uncheckedParser extensions = space *> manyUncheckedElement <* eof manyUncheckedElement :: Parser UNode manyUncheckedElement = do children <- many (try uncheckedElement <|> uncheckedTextNode <|> uncheckedSplicedNode) - case children of - [node] -> return node - _ -> return $ UNode "div" [] children False + return $ UNode "div" [] children False uncheckedElement :: Parser UNode uncheckedElement = do @@ -67,15 +60,13 @@ uncheckedElement = do tagName <- T.pack <$> some (alphaNumChar <|> char '-' <|> char '_' <|> char ':') attrs <- many uncheckedAttribute space - if Set.member tagName selfClosingTags - then (void (string "/>" <|> string ">")) >> return (UNode tagName attrs [] True) + if tagName `Set.member` voidElements + then (void (string "/>" <|> string ">") >> return (UNode tagName attrs [] True)) else do void $ char '>' children <- many (try uncheckedElement <|> uncheckedTextNode <|> uncheckedSplicedNode) - closing <- optional (try $ string " chunk tagName *> char '>') - case closing of - Just _ -> return $ UNode tagName attrs children False - Nothing -> fail $ "Unclosed tag: <" ++ T.unpack tagName ++ ">" + void $ string " chunk tagName *> char '>' + return $ UNode tagName attrs children False uncheckedAttribute :: Parser (Text, AttributeValue) uncheckedAttribute = do @@ -84,7 +75,7 @@ uncheckedAttribute = do value <- option (TextValue "") (char '=' *> (quotedValue <|> unquotedValue <|> expressionValue)) return (name, value) where - quotedValue = TextValue . T.pack <$> (char '"' *> manyTill L.charLiteral (char '"')) + quotedValue = TextValue . T.pack <$> (char '"' *> manyTill anySingle (char '"')) unquotedValue = TextValue . T.pack <$> some (alphaNumChar <|> char '-' <|> char '_') expressionValue = ExpressionValue <$> (char '{' *> parseHaskellExpression <* char '}') @@ -103,56 +94,27 @@ parseHaskellExpression = do Left err -> fail $ show err compileToHaskell :: UNode -> Q Exp -compileToHaskell (UNode name attrs children isLeaf) = do - when (T.toLower name `notElem` knownElements && T.toLower name `notElem` knownLeafs) $ - case validateCustomElement name of - Left err -> fail err - Right () -> pure () - when (not isLeaf && null children && name `notElem` voidElements) $ - fail $ "Empty non-void element: <" ++ T.unpack name ++ ">" - let element = if isLeaf || name `elem` voidElements - then nodeToBlazeLeaf name - else nodeToBlazeElement name +compileToHaskell (UNode name attrs children isLeaf) = + let element = [| H.preEscapedText $(litE $ stringL $ "<" ++ T.unpack name) |] applyAttrs = foldr (\(k, v) e -> [| $e <> $(compileAttribute k v) |]) element attrs closeTag = if isLeaf - then [| mempty |] - else [| H.preEscapedText $(litE $ stringL $ "") |] + then [| H.preEscapedText $(litE $ stringL "/>") |] + else [| H.preEscapedText $(litE $ stringL ">") |] applyChildren = if null children - then [| $applyAttrs |] - else [| $applyAttrs <> mconcat $(listE (map compileToHaskell children)) |] - [| $applyChildren <> $closeTag |] -compileToHaskell (UTextNode value) = [| H.text $(litE $ stringL $ T.unpack value) |] + then [| $applyAttrs <> $closeTag |] + else [| $applyAttrs <> H.preEscapedText $(litE $ stringL ">") <> + mconcat $(listE (map compileToHaskell children)) <> + H.preEscapedText $(litE $ stringL $ "") |] + in applyChildren +compileToHaskell (UTextNode text) = [| H.text $(litE $ stringL $ T.unpack text) |] compileToHaskell (USplicedNode exp) = [| H.preEscapedToHtml $(return exp) |] -compileToHaskell (UCommentNode value) = [| H.textComment $(litE $ stringL $ T.unpack value) |] -compileToHaskell UNoRenderCommentNode = [| mempty |] - -selfClosingTags :: Set.Set Text -selfClosingTags = Set.fromList - [ "area", "base", "br", "col", "embed", "hr", "img", "input" - , "link", "meta", "param", "source", "track", "wbr" - ] - -voidElements :: [Text] -voidElements = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"] - -validateCustomElement :: Text -> Either String () -validateCustomElement name - | T.any (== '-') name && not (isLower (T.head name)) = Left $ "Custom element '" ++ T.unpack name ++ "' must start with a lowercase letter" - | T.any (== '-') name = Right () - | otherwise = Left $ "Custom element '" ++ T.unpack name ++ "' must contain a hyphen (-) and start with a lowercase letter" - -knownElements :: [Text] -knownElements = ["div", "span", "p", "a", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "table", "tr", "td", "th", "form", "input", "button", "select", "option", "textarea", "label", "header", "footer", "nav", "main", "section", "article", "aside"] - -knownLeafs :: [Text] -knownLeafs = ["br", "hr", "img", "input", "meta", "link"] - -nodeToBlazeLeaf :: Text -> Q Exp -nodeToBlazeLeaf name = [| H.preEscapedText $(litE $ stringL $ "<" ++ T.unpack name ++ "/>") |] - -nodeToBlazeElement :: Text -> Q Exp -nodeToBlazeElement name = [| H.preEscapedText $(litE $ stringL $ "<" ++ T.unpack name ++ ">") |] compileAttribute :: Text -> AttributeValue -> Q Exp compileAttribute name (TextValue value) = [| H.preEscapedText $(litE $ stringL $ " " ++ T.unpack name ++ "=\"" ++ T.unpack value ++ "\"") |] compileAttribute name (ExpressionValue exp) = [| H.preEscapedText (T.pack $ " " ++ T.unpack name ++ "=\"") <> H.toHtml $(return exp) <> H.preEscapedText "\"" |] + +voidElements :: Set Text +voidElements = Set.fromList + [ "area", "base", "br", "col", "embed", "hr", "img", "input" + , "link", "meta", "param", "source", "track", "wbr" + ] From 6f6480a69e7985a636713c39b19aab962e58f551 Mon Sep 17 00:00:00 2001 From: lillo Date: Thu, 31 Oct 2024 16:54:25 +0700 Subject: [PATCH 05/29] remove UncheckedHSX and attept to restore QQ and Parser modules --- ihp-hsx/IHP/HSX/Parser.hs | 2 +- ihp-hsx/IHP/HSX/QQ.hs | 6 +- ihp-hsx/IHP/HSX/UncheckedHSX.hs | 120 -------------------------------- ihp-hsx/ihp-hsx.cabal | 1 - 4 files changed, 3 insertions(+), 126 deletions(-) delete mode 100644 ihp-hsx/IHP/HSX/UncheckedHSX.hs diff --git a/ihp-hsx/IHP/HSX/Parser.hs b/ihp-hsx/IHP/HSX/Parser.hs index 1f50d1d5c..78bd94290 100644 --- a/ihp-hsx/IHP/HSX/Parser.hs +++ b/ihp-hsx/IHP/HSX/Parser.hs @@ -625,4 +625,4 @@ collapseSpace text = cs $ filterDuplicateSpaces (cs text) filterDuplicateSpaces' (char:rest) True | Char.isSpace char = filterDuplicateSpaces' rest True filterDuplicateSpaces' (char:rest) False | Char.isSpace char = ' ':(filterDuplicateSpaces' rest True) filterDuplicateSpaces' (char:rest) isRemovingSpaces = char:(filterDuplicateSpaces' rest False) - filterDuplicateSpaces' [] isRemovingSpaces = [] + filterDuplicateSpaces' [] isRemovingSpaces = [] \ No newline at end of file diff --git a/ihp-hsx/IHP/HSX/QQ.hs b/ihp-hsx/IHP/HSX/QQ.hs index 7b4cf7ddf..cb0df412d 100644 --- a/ihp-hsx/IHP/HSX/QQ.hs +++ b/ihp-hsx/IHP/HSX/QQ.hs @@ -5,7 +5,7 @@ Module: IHP.HSX.QQ Description: Defines the @[hsx||]@ syntax Copyright: (c) digitally induced GmbH, 2022 -} -module IHP.HSX.QQ (hsx, uncheckedHsx) where +module IHP.HSX.QQ (hsx) where import Prelude import Data.Text (Text) @@ -27,7 +27,6 @@ import Data.List (foldl') import IHP.HSX.Attribute import qualified Text.Blaze.Html5.Attributes as Attributes import qualified Data.HashMap.Strict as HashMap -import IHP.HSX.UncheckedHSX (uncheckedHsx) hsx :: QuasiQuoter hsx = QuasiQuoter { @@ -459,5 +458,4 @@ textToStaticString text = StaticString (Text.unpack text ++) (Text.encodeUtf8 te {-# INLINE textToStaticString #-} instance Show (MarkupM ()) where - show html = BlazeString.renderHtml html - + show html = BlazeString.renderHtml html \ No newline at end of file diff --git a/ihp-hsx/IHP/HSX/UncheckedHSX.hs b/ihp-hsx/IHP/HSX/UncheckedHSX.hs deleted file mode 100644 index c929433fb..000000000 --- a/ihp-hsx/IHP/HSX/UncheckedHSX.hs +++ /dev/null @@ -1,120 +0,0 @@ -{-# LANGUAGE TemplateHaskell, QuasiQuotes, OverloadedStrings #-} -module IHP.HSX.UncheckedHSX (uncheckedHsx) where - -import Prelude -import Language.Haskell.TH -import Language.Haskell.TH.Quote -import Text.Blaze.Html5 ((!)) -import qualified Text.Blaze.Html5 as H -import qualified Text.Blaze.Html5.Attributes as A -import Data.Text (Text) -import qualified Data.Text as T -import Text.Megaparsec -import Text.Megaparsec.Char -import Data.Void -import Data.String.Conversions (cs) -import qualified IHP.HSX.HaskellParser as HaskellParser -import Text.Blaze.Internal (MarkupM(Parent, Leaf), attribute) -import qualified Data.List.NonEmpty as NE -import Data.Functor (void) -import Data.Set (Set) -import qualified Data.Set as Set - -type Parser = Parsec Void Text - -data UNode = UNode Text [(Text, AttributeValue)] [UNode] Bool - | UTextNode Text - | USplicedNode Exp - deriving (Show) - -data AttributeValue = TextValue Text | ExpressionValue Exp deriving (Show) - -uncheckedHsx :: QuasiQuoter -uncheckedHsx = QuasiQuoter - { quoteExp = quoteUncheckedHsxExpression - , quotePat = error "quotePat not implemented for uncheckedHsx" - , quoteType = error "quoteType not implemented for uncheckedHsx" - , quoteDec = error "quoteDec not implemented for uncheckedHsx" - } - -quoteUncheckedHsxExpression :: String -> Q Exp -quoteUncheckedHsxExpression code = do - loc <- location - let position = SourcePos (loc_filename loc) (mkPos (fst (loc_start loc))) (mkPos (snd (loc_start loc))) - extensions <- extsEnabled - case runParser (uncheckedParser extensions) "" (cs code) of - Left err -> fail $ errorBundlePretty err - Right result -> compileToHaskell result - -uncheckedParser :: [Extension] -> Parser UNode -uncheckedParser extensions = space *> manyUncheckedElement <* eof - -manyUncheckedElement :: Parser UNode -manyUncheckedElement = do - children <- many (try uncheckedElement <|> uncheckedTextNode <|> uncheckedSplicedNode) - return $ UNode "div" [] children False - -uncheckedElement :: Parser UNode -uncheckedElement = do - void $ char '<' - tagName <- T.pack <$> some (alphaNumChar <|> char '-' <|> char '_' <|> char ':') - attrs <- many uncheckedAttribute - space - if tagName `Set.member` voidElements - then (void (string "/>" <|> string ">") >> return (UNode tagName attrs [] True)) - else do - void $ char '>' - children <- many (try uncheckedElement <|> uncheckedTextNode <|> uncheckedSplicedNode) - void $ string " chunk tagName *> char '>' - return $ UNode tagName attrs children False - -uncheckedAttribute :: Parser (Text, AttributeValue) -uncheckedAttribute = do - space - name <- T.pack <$> some (alphaNumChar <|> char '-' <|> char '_' <|> char ':') - value <- option (TextValue "") (char '=' *> (quotedValue <|> unquotedValue <|> expressionValue)) - return (name, value) - where - quotedValue = TextValue . T.pack <$> (char '"' *> manyTill anySingle (char '"')) - unquotedValue = TextValue . T.pack <$> some (alphaNumChar <|> char '-' <|> char '_') - expressionValue = ExpressionValue <$> (char '{' *> parseHaskellExpression <* char '}') - -uncheckedTextNode :: Parser UNode -uncheckedTextNode = UTextNode . T.pack <$> some (satisfy (\c -> c /= '<' && c /= '{' && c /= '}')) - -uncheckedSplicedNode :: Parser UNode -uncheckedSplicedNode = USplicedNode <$> (char '{' *> parseHaskellExpression <* char '}') - -parseHaskellExpression :: Parser Exp -parseHaskellExpression = do - pos <- getSourcePos - code <- takeWhileP Nothing (\c -> c /= '}') - case HaskellParser.parseHaskellExpression pos [] (T.unpack code) of - Right exp -> return exp - Left err -> fail $ show err - -compileToHaskell :: UNode -> Q Exp -compileToHaskell (UNode name attrs children isLeaf) = - let element = [| H.preEscapedText $(litE $ stringL $ "<" ++ T.unpack name) |] - applyAttrs = foldr (\(k, v) e -> [| $e <> $(compileAttribute k v) |]) element attrs - closeTag = if isLeaf - then [| H.preEscapedText $(litE $ stringL "/>") |] - else [| H.preEscapedText $(litE $ stringL ">") |] - applyChildren = if null children - then [| $applyAttrs <> $closeTag |] - else [| $applyAttrs <> H.preEscapedText $(litE $ stringL ">") <> - mconcat $(listE (map compileToHaskell children)) <> - H.preEscapedText $(litE $ stringL $ "") |] - in applyChildren -compileToHaskell (UTextNode text) = [| H.text $(litE $ stringL $ T.unpack text) |] -compileToHaskell (USplicedNode exp) = [| H.preEscapedToHtml $(return exp) |] - -compileAttribute :: Text -> AttributeValue -> Q Exp -compileAttribute name (TextValue value) = [| H.preEscapedText $(litE $ stringL $ " " ++ T.unpack name ++ "=\"" ++ T.unpack value ++ "\"") |] -compileAttribute name (ExpressionValue exp) = [| H.preEscapedText (T.pack $ " " ++ T.unpack name ++ "=\"") <> H.toHtml $(return exp) <> H.preEscapedText "\"" |] - -voidElements :: Set Text -voidElements = Set.fromList - [ "area", "base", "br", "col", "embed", "hr", "img", "input" - , "link", "meta", "param", "source", "track", "wbr" - ] diff --git a/ihp-hsx/ihp-hsx.cabal b/ihp-hsx/ihp-hsx.cabal index 42df73cea..8ee565b4b 100644 --- a/ihp-hsx/ihp-hsx.cabal +++ b/ihp-hsx/ihp-hsx.cabal @@ -89,7 +89,6 @@ library , IHP.HSX.HaskellParser , IHP.HSX.HsExpToTH , IHP.HSX.Attribute - , IHP.HSX.UncheckedHSX test-suite ihp-hsx-tests type: exitcode-stdio-1.0 From ef158e4035fb2f9e28cdd223d85da60d98db55dc Mon Sep 17 00:00:00 2001 From: lillo Date: Thu, 31 Oct 2024 16:55:32 +0700 Subject: [PATCH 06/29] reset some unecessary whitespace stuff --- ihp-hsx/IHP/HSX/Parser.hs | 2 +- ihp-hsx/IHP/HSX/QQ.hs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ihp-hsx/IHP/HSX/Parser.hs b/ihp-hsx/IHP/HSX/Parser.hs index 78bd94290..1f50d1d5c 100644 --- a/ihp-hsx/IHP/HSX/Parser.hs +++ b/ihp-hsx/IHP/HSX/Parser.hs @@ -625,4 +625,4 @@ collapseSpace text = cs $ filterDuplicateSpaces (cs text) filterDuplicateSpaces' (char:rest) True | Char.isSpace char = filterDuplicateSpaces' rest True filterDuplicateSpaces' (char:rest) False | Char.isSpace char = ' ':(filterDuplicateSpaces' rest True) filterDuplicateSpaces' (char:rest) isRemovingSpaces = char:(filterDuplicateSpaces' rest False) - filterDuplicateSpaces' [] isRemovingSpaces = [] \ No newline at end of file + filterDuplicateSpaces' [] isRemovingSpaces = [] diff --git a/ihp-hsx/IHP/HSX/QQ.hs b/ihp-hsx/IHP/HSX/QQ.hs index cb0df412d..bdd732d08 100644 --- a/ihp-hsx/IHP/HSX/QQ.hs +++ b/ihp-hsx/IHP/HSX/QQ.hs @@ -458,4 +458,5 @@ textToStaticString text = StaticString (Text.unpack text ++) (Text.encodeUtf8 te {-# INLINE textToStaticString #-} instance Show (MarkupM ()) where - show html = BlazeString.renderHtml html \ No newline at end of file + show html = BlazeString.renderHtml html + \ No newline at end of file From ba5bc56c4d6e00e5247121fc267754881291795f Mon Sep 17 00:00:00 2001 From: lillo Date: Thu, 31 Oct 2024 16:56:28 +0700 Subject: [PATCH 07/29] remove unecessary whitespace --- ihp-hsx/IHP/HSX/QQ.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ihp-hsx/IHP/HSX/QQ.hs b/ihp-hsx/IHP/HSX/QQ.hs index bdd732d08..cb0df412d 100644 --- a/ihp-hsx/IHP/HSX/QQ.hs +++ b/ihp-hsx/IHP/HSX/QQ.hs @@ -458,5 +458,4 @@ textToStaticString text = StaticString (Text.unpack text ++) (Text.encodeUtf8 te {-# INLINE textToStaticString #-} instance Show (MarkupM ()) where - show html = BlazeString.renderHtml html - \ No newline at end of file + show html = BlazeString.renderHtml html \ No newline at end of file From d5b95f552d092d895865ac0c55375dcbea66a29f Mon Sep 17 00:00:00 2001 From: lillo Date: Thu, 31 Oct 2024 18:14:29 +0700 Subject: [PATCH 08/29] unchecked HSX working, with tests on the parser --- ihp-hsx/IHP/HSX/Parser.hs | 14 ++--- ihp-hsx/IHP/HSX/QQ.hs | 18 ++++-- ihp-hsx/Test/IHP/HSX/ParserSpec.hs | 96 +++++++++++++++++++++++++----- 3 files changed, 101 insertions(+), 27 deletions(-) diff --git a/ihp-hsx/IHP/HSX/Parser.hs b/ihp-hsx/IHP/HSX/Parser.hs index 1f50d1d5c..6d000634b 100644 --- a/ihp-hsx/IHP/HSX/Parser.hs +++ b/ihp-hsx/IHP/HSX/Parser.hs @@ -57,15 +57,16 @@ data Node = Node !Text ![Attribute] ![Node] !Bool -- > let position = Megaparsec.SourcePos filePath (Megaparsec.mkPos line) (Megaparsec.mkPos col) -- > let hsxText = "Hello" -- > --- > let (Right node) = parseHsx position [] hsxText -parseHsx :: SourcePos -> [TH.Extension] -> Text -> Either (ParseErrorBundle Text Void) Node -parseHsx position extensions code = +-- > let (Right node) = parseHsx True position [] hsxText +parseHsx :: Bool -> SourcePos -> [TH.Extension] -> Text -> Either (ParseErrorBundle Text Void) Node +parseHsx checkMarkup position extensions code = let ?extensions = extensions + ?checkMarkup = checkMarkup in runParser (setPosition position *> parser) "" code -type Parser a = (?extensions :: [TH.Extension]) => Parsec Void Text a +type Parser a = (?extensions :: [TH.Extension], ?checkMarkup :: Bool) => Parsec Void Text a setPosition pstateSourcePos = updateParserState (\state -> state { statePosState = (statePosState state) { pstateSourcePos } @@ -211,14 +212,13 @@ hsxNodeAttribute = do hsxAttributeName :: Parser Text hsxAttributeName = do name <- rawAttribute - unless (isValidAttributeName name) (fail $ "Invalid attribute name: " <> cs name) + unless (isValidAttributeName name || not ?checkMarkup) (fail $ "Invalid attribute name: " <> cs name) pure name where isValidAttributeName name = "data-" `Text.isPrefixOf` name || "aria-" `Text.isPrefixOf` name || "hx-" `Text.isPrefixOf` name - || "hx-" `Text.isPrefixOf` name || name `Set.member` attributes rawAttribute = takeWhile1P Nothing (\c -> Char.isAlphaNum c || c == '-' || c == '_') @@ -291,7 +291,7 @@ hsxElementName = do let isValidParent = name `Set.member` parents let isValidLeaf = name `Set.member` leafs let isValidCustomWebComponent = "-" `Text.isInfixOf` name - unless (isValidParent || isValidLeaf || isValidCustomWebComponent) (fail $ "Invalid tag name: " <> cs name) + unless (isValidParent || isValidLeaf || isValidCustomWebComponent || not ?checkMarkup) (fail $ "Invalid tag name: " <> cs name) space pure name diff --git a/ihp-hsx/IHP/HSX/QQ.hs b/ihp-hsx/IHP/HSX/QQ.hs index cb0df412d..4720c9604 100644 --- a/ihp-hsx/IHP/HSX/QQ.hs +++ b/ihp-hsx/IHP/HSX/QQ.hs @@ -5,7 +5,7 @@ Module: IHP.HSX.QQ Description: Defines the @[hsx||]@ syntax Copyright: (c) digitally induced GmbH, 2022 -} -module IHP.HSX.QQ (hsx) where +module IHP.HSX.QQ (hsx, uncheckedHsx) where import Prelude import Data.Text (Text) @@ -30,17 +30,25 @@ import qualified Data.HashMap.Strict as HashMap hsx :: QuasiQuoter hsx = QuasiQuoter { - quoteExp = quoteHsxExpression, + quoteExp = quoteHsxExpression True, quotePat = error "quotePat: not defined", quoteDec = error "quoteDec: not defined", quoteType = error "quoteType: not defined" } -quoteHsxExpression :: String -> TH.ExpQ -quoteHsxExpression code = do +uncheckedHsx :: QuasiQuoter +uncheckedHsx = QuasiQuoter { + quoteExp = quoteHsxExpression False, + quotePat = error "quotePat: not defined", + quoteDec = error "quoteDec: not defined", + quoteType = error "quoteType: not defined" + } + +quoteHsxExpression :: Bool -> String -> TH.ExpQ +quoteHsxExpression checkMarkup code = do hsxPosition <- findHSXPosition extensions <- TH.extsEnabled - expression <- case parseHsx hsxPosition extensions (cs code) of + expression <- case parseHsx checkMarkup hsxPosition extensions (cs code) of Left error -> fail (Megaparsec.errorBundlePretty error) Right result -> pure result compileToHaskell expression diff --git a/ihp-hsx/Test/IHP/HSX/ParserSpec.hs b/ihp-hsx/Test/IHP/HSX/ParserSpec.hs index 11b6c1506..ad936b7f3 100644 --- a/ihp-hsx/Test/IHP/HSX/ParserSpec.hs +++ b/ihp-hsx/Test/IHP/HSX/ParserSpec.hs @@ -15,64 +15,130 @@ import qualified "template-haskell" Language.Haskell.TH.Syntax as TH tests = do let position = Megaparsec.SourcePos "" (Megaparsec.mkPos 1) (Megaparsec.mkPos 1) let extensions = [] + describe "HSX Parser" do + let checkMarkup = True it "should fail on invalid html tags" do let errorText = "1:13:\n |\n1 | \n | ^\nInvalid tag name: myinvalidel\n" - let (Left error) = parseHsx position extensions "" + let (Left error) = parseHsx checkMarkup position extensions "" (Megaparsec.errorBundlePretty error) `shouldBe` errorText it "should fail on invalid attribute names" do let errorText = "1:23:\n |\n1 |
\n | ^\nInvalid attribute name: invalid-attribute\n" - let (Left error) = parseHsx position extensions "
" + let (Left error) = parseHsx checkMarkup position extensions "
" (Megaparsec.errorBundlePretty error) `shouldBe` errorText it "should fail on unmatched tags" do let errorText = "1:7:\n |\n1 |
\n | ^\nunexpected '/'\nexpecting \"
\", identifier, or white space\n" - let (Left error) = parseHsx position extensions "
" + let (Left error) = parseHsx checkMarkup position extensions "
" (Megaparsec.errorBundlePretty error) `shouldBe` errorText it "should parse a closing tag with spaces" do - let p = parseHsx position extensions "
" + let p = parseHsx checkMarkup position extensions "
" p `shouldBe` (Right (Children [Node "div" [] [] False])) it "should strip spaces around nodes" do - let p = parseHsx position extensions "
" + let p = parseHsx checkMarkup position extensions "
" p `shouldBe` (Right (Children [Node "div" [] [Node "span" [] [] False] False])) it "should strip spaces after self closing tags" do - let p = parseHsx position extensions "{\"meta\"}\n\n " + let p = parseHsx checkMarkup position extensions "{\"meta\"}\n\n " p `shouldBe` (Right (Children [Node "head" [] [SplicedNode (TH.LitE (TH.StringL "meta")),Node "link" [StaticAttribute "rel" (TextValue "stylesheet"),StaticAttribute "href" (TextValue "/vendor/bootstrap.min.css")] [] True] False])) it "should not strip spaces in a text node" do - let p = parseHsx position extensions " Hello World " + let p = parseHsx checkMarkup position extensions " Hello World " p `shouldBe` (Right (Children [TextNode "Hello World"])) it "should deal with variables in text nodes" do - let p = parseHsx position extensions "
\n Hello {\"name\"}! \n
" + let p = parseHsx checkMarkup position extensions "
\n Hello {\"name\"}! \n
" p `shouldBe` (Right (Children [Node "div" [] [TextNode "Hello ",SplicedNode (TH.LitE (TH.StringL "name")),TextNode "!"] False])) it "should parse self closing tags with spaces around it" do - let p = parseHsx position extensions "
" + let p = parseHsx checkMarkup position extensions "
" p `shouldBe` (Right (Children [Node "div" [] [] False])) it "should collapse spaces" do - let p = parseHsx position extensions "\n Hello\n World\n ! " + let p = parseHsx checkMarkup position extensions "\n Hello\n World\n ! " p `shouldBe` (Right (Children [TextNode "Hello World !"])) it "should parse spread values" do - let p = parseHsx position extensions "
" + let p = parseHsx checkMarkup position extensions "
" -- We cannot easily construct the @VarE variables@ expression, therefore we use show here for comparison show p `shouldBe` "Right (Children [Node \"div\" [SpreadAttributes (VarE variables)] [] False])" it "should parse spread values with a space" do -- See https://github.com/digitallyinduced/ihp/issues/1588 - let p = parseHsx position extensions "
" + let p = parseHsx checkMarkup position extensions "
" show p `shouldBe` "Right (Children [Node \"div\" [SpreadAttributes (VarE variables)] [] False])" it "should accept underscores in data attributes" do - let p = parseHsx position extensions "
" + let p = parseHsx checkMarkup position extensions "
" p `shouldBe` (Right (Children [Node "div" [StaticAttribute "data-client_id" (TextValue "test")] [] False])) it "should accept doctype" do - let p = parseHsx position extensions "hello" - p `shouldBe` (Right (Children [Node "!DOCTYPE" [StaticAttribute "html" (TextValue "html")] [] True, Node "html" [StaticAttribute "lang" (TextValue "en")] [Node "body" [] [TextNode "hello"] False] False])) \ No newline at end of file + let p = parseHsx checkMarkup position extensions "hello" + p `shouldBe` (Right (Children [Node "!DOCTYPE" [StaticAttribute "html" (TextValue "html")] [] True, Node "html" [StaticAttribute "lang" (TextValue "en")] [Node "body" [] [TextNode "hello"] False] False])) + + describe "uncheckedHsx" do + let checkMarkup = False + it "should not check markup" do + let p = parseHsx False position extensions "" + p `shouldBe` (Right (Children [Node "invalid-tag" [StaticAttribute "invalid-attribute" (TextValue "invalid")] [] False])) + + it "should not check attribute names" do + let p = parseHsx False position extensions "
" + p `shouldBe` (Right (Children [Node "div" [StaticAttribute "invalid-attribute" (TextValue "invalid")] [] False])) + + it "should fail on unmatched tags" do + let errorText = "1:7:\n |\n1 |
\n | ^\nunexpected '/'\nexpecting \"
\", identifier, or white space\n" + let (Left error) = parseHsx checkMarkup position extensions "
" + (Megaparsec.errorBundlePretty error) `shouldBe` errorText + + it "should parse a closing tag with spaces" do + let p = parseHsx checkMarkup position extensions "
" + p `shouldBe` (Right (Children [Node "div" [] [] False])) + + it "should strip spaces around nodes" do + let p = parseHsx checkMarkup position extensions "
" + p `shouldBe` (Right (Children [Node "div" [] [Node "span" [] [] False] False])) + + it "should strip spaces after self closing tags" do + let p = parseHsx checkMarkup position extensions "{\"meta\"}\n\n " + p `shouldBe` (Right (Children [Node "head" [] [SplicedNode (TH.LitE (TH.StringL "meta")),Node "link" [StaticAttribute "rel" (TextValue "stylesheet"),StaticAttribute "href" (TextValue "/vendor/bootstrap.min.css")] [] True] False])) + + it "should not strip spaces in a text node" do + let p = parseHsx checkMarkup position extensions " Hello World " + p `shouldBe` (Right (Children [TextNode "Hello World"])) + + it "should deal with variables in text nodes" do + let p = parseHsx checkMarkup position extensions "
\n Hello {\"name\"}! \n
" + p `shouldBe` (Right (Children [Node "div" [] [TextNode "Hello ",SplicedNode (TH.LitE (TH.StringL "name")),TextNode "!"] False])) + + it "should parse self closing tags with spaces around it" do + let p = parseHsx checkMarkup position extensions "
" + p `shouldBe` (Right (Children [Node "div" [] [] False])) + + it "should collapse spaces" do + let p = parseHsx checkMarkup position extensions "\n Hello\n World\n ! " + p `shouldBe` (Right (Children [TextNode "Hello World !"])) + + it "should parse spread values" do + let p = parseHsx checkMarkup position extensions "
" + -- We cannot easily construct the @VarE variables@ expression, therefore we use show here for comparison + show p `shouldBe` "Right (Children [Node \"div\" [SpreadAttributes (VarE variables)] [] False])" + + it "should parse spread values with a space" do + -- See https://github.com/digitallyinduced/ihp/issues/1588 + let p = parseHsx checkMarkup position extensions "
" + show p `shouldBe` "Right (Children [Node \"div\" [SpreadAttributes (VarE variables)] [] False])" + + it "should accept underscores in data attributes" do + let p = parseHsx checkMarkup position extensions "
" + p `shouldBe` (Right (Children [Node "div" [StaticAttribute "data-client_id" (TextValue "test")] [] False])) + + it "should accept doctype" do + let p = parseHsx checkMarkup position extensions "hello" + p `shouldBe` (Right (Children [Node "!DOCTYPE" [StaticAttribute "html" (TextValue "html")] [] True, Node "html" [StaticAttribute "lang" (TextValue "en")] [Node "body" [] [TextNode "hello"] False] False])) + + + From f54430bb33f4b79462bb138e6a5967d2819885ed Mon Sep 17 00:00:00 2001 From: lillo Date: Thu, 31 Oct 2024 21:04:19 +0700 Subject: [PATCH 09/29] Add customHsx + tests --- ihp-hsx/IHP/HSX/Parser.hs | 28 ++++++-- ihp-hsx/IHP/HSX/QQ.hs | 24 +++++-- ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs | 28 ++++++++ ihp-hsx/Test/IHP/HSX/ParserSpec.hs | 91 +++++++++++++++++--------- ihp-hsx/Test/IHP/HSX/QQSpec.hs | 16 +++++ 5 files changed, 143 insertions(+), 44 deletions(-) create mode 100644 ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs diff --git a/ihp-hsx/IHP/HSX/Parser.hs b/ihp-hsx/IHP/HSX/Parser.hs index 6d000634b..e78fc12bf 100644 --- a/ihp-hsx/IHP/HSX/Parser.hs +++ b/ihp-hsx/IHP/HSX/Parser.hs @@ -15,6 +15,7 @@ module IHP.HSX.Parser , Attribute (..) , AttributeValue (..) , collapseSpace +, HsxSettings (..) ) where import Prelude @@ -34,6 +35,12 @@ import qualified Data.Set as Set import qualified Data.Containers.ListUtils as List import qualified IHP.HSX.HaskellParser as HaskellParser +data HsxSettings = HsxSettings { + checkMarkup :: Bool, + additionalTagNames :: Set Text, + additionalAttributeNames :: Set Text +} + data AttributeValue = TextValue !Text | ExpressionValue !Haskell.Exp deriving (Eq, Show) data Attribute = StaticAttribute !Text !AttributeValue | SpreadAttributes !Haskell.Exp deriving (Eq, Show) @@ -58,15 +65,15 @@ data Node = Node !Text ![Attribute] ![Node] !Bool -- > let hsxText = "Hello" -- > -- > let (Right node) = parseHsx True position [] hsxText -parseHsx :: Bool -> SourcePos -> [TH.Extension] -> Text -> Either (ParseErrorBundle Text Void) Node -parseHsx checkMarkup position extensions code = +parseHsx :: HsxSettings -> SourcePos -> [TH.Extension] -> Text -> Either (ParseErrorBundle Text Void) Node +parseHsx settings position extensions code = let ?extensions = extensions - ?checkMarkup = checkMarkup + ?settings = settings in runParser (setPosition position *> parser) "" code -type Parser a = (?extensions :: [TH.Extension], ?checkMarkup :: Bool) => Parsec Void Text a +type Parser a = (?extensions :: [TH.Extension], ?settings :: HsxSettings) => Parsec Void Text a setPosition pstateSourcePos = updateParserState (\state -> state { statePosState = (statePosState state) { pstateSourcePos } @@ -212,7 +219,9 @@ hsxNodeAttribute = do hsxAttributeName :: Parser Text hsxAttributeName = do name <- rawAttribute - unless (isValidAttributeName name || not ?checkMarkup) (fail $ "Invalid attribute name: " <> cs name) + let shouldCheckMarkup = ?settings.checkMarkup + let isValidAdditionalAttribute = name `Set.member` ?settings.additionalAttributeNames + unless (isValidAttributeName name || not shouldCheckMarkup || isValidAdditionalAttribute) (fail $ "Invalid attribute name: " <> cs name) pure name where isValidAttributeName name = @@ -285,13 +294,18 @@ hsxSplicedNode = do treeToString acc (TokenNode (x:xs)) = ((treeToString (acc <> "{") x) <> (Text.concat $ fmap (treeToString "") xs)) <> "}" + hsxElementName :: Parser Text hsxElementName = do name <- takeWhile1P (Just "identifier") (\c -> Char.isAlphaNum c || c == '_' || c == '-' || c == '!') let isValidParent = name `Set.member` parents let isValidLeaf = name `Set.member` leafs - let isValidCustomWebComponent = "-" `Text.isInfixOf` name - unless (isValidParent || isValidLeaf || isValidCustomWebComponent || not ?checkMarkup) (fail $ "Invalid tag name: " <> cs name) + let isValidCustomWebComponent = "-" `Text.isInfixOf` name + && not (Text.isPrefixOf "-" name) + && not (Char.isNumber (Text.head name)) + let isValidAdditionalTag = name `Set.member` ?settings.additionalTagNames + let shouldCheckMarkup = ?settings.checkMarkup + unless (isValidParent || isValidLeaf || isValidCustomWebComponent || isValidAdditionalTag || not shouldCheckMarkup) (fail $ "Invalid tag name: " <> cs name) space pure name diff --git a/ihp-hsx/IHP/HSX/QQ.hs b/ihp-hsx/IHP/HSX/QQ.hs index 4720c9604..901562f3c 100644 --- a/ihp-hsx/IHP/HSX/QQ.hs +++ b/ihp-hsx/IHP/HSX/QQ.hs @@ -5,7 +5,7 @@ Module: IHP.HSX.QQ Description: Defines the @[hsx||]@ syntax Copyright: (c) digitally induced GmbH, 2022 -} -module IHP.HSX.QQ (hsx, uncheckedHsx) where +module IHP.HSX.QQ (hsx, uncheckedHsx, customHsx, AdditionalTags(..), AdditionalAttributes(..)) where import Prelude import Data.Text (Text) @@ -27,10 +27,11 @@ import Data.List (foldl') import IHP.HSX.Attribute import qualified Text.Blaze.Html5.Attributes as Attributes import qualified Data.HashMap.Strict as HashMap +import qualified Data.Set as Set hsx :: QuasiQuoter hsx = QuasiQuoter { - quoteExp = quoteHsxExpression True, + quoteExp = quoteHsxExpression (HsxSettings True Set.empty Set.empty), quotePat = error "quotePat: not defined", quoteDec = error "quoteDec: not defined", quoteType = error "quoteType: not defined" @@ -38,17 +39,28 @@ hsx = QuasiQuoter { uncheckedHsx :: QuasiQuoter uncheckedHsx = QuasiQuoter { - quoteExp = quoteHsxExpression False, + quoteExp = quoteHsxExpression (HsxSettings False Set.empty Set.empty), quotePat = error "quotePat: not defined", quoteDec = error "quoteDec: not defined", quoteType = error "quoteType: not defined" } -quoteHsxExpression :: Bool -> String -> TH.ExpQ -quoteHsxExpression checkMarkup code = do +newtype AdditionalTags = AdditionalTags [Text] +newtype AdditionalAttributes = AdditionalAttributes [Text] + +customHsx :: AdditionalTags -> AdditionalAttributes -> QuasiQuoter +customHsx (AdditionalTags additionalTagNames) (AdditionalAttributes additionalAttributeNames) = QuasiQuoter { + quoteExp = quoteHsxExpression (HsxSettings True (Set.fromList additionalTagNames) (Set.fromList additionalAttributeNames)), + quotePat = error "quotePat: not defined", + quoteDec = error "quoteDec: not defined", + quoteType = error "quoteType: not defined" + } + +quoteHsxExpression :: HsxSettings -> String -> TH.ExpQ +quoteHsxExpression settings code = do hsxPosition <- findHSXPosition extensions <- TH.extsEnabled - expression <- case parseHsx checkMarkup hsxPosition extensions (cs code) of + expression <- case parseHsx settings hsxPosition extensions (cs code) of Left error -> fail (Megaparsec.errorBundlePretty error) Right result -> pure result compileToHaskell expression diff --git a/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs b/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs new file mode 100644 index 000000000..4763b6039 --- /dev/null +++ b/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs @@ -0,0 +1,28 @@ +{-| +Module: IHP.HSX.TestHsx +Description: Test helpers for HSX tests with custom tags and attributes +-} +module IHP.HSX.CustomHsxCases where + +import Test.Hspec +import Prelude +import IHP.HSX.QQ +import qualified Text.Blaze.Renderer.Text as Blaze +import Data.Text +import Language.Haskell.TH.Quote +import IHP.HSX.Parser + +myCustomHsx :: QuasiQuoter +myCustomHsx = customHsx + (AdditionalTags ["mycustomtag", "anothercustomtag"]) + (AdditionalAttributes ["my-custom-attr", "anothercustomattr"]) + +myTagsOnlyHsx :: QuasiQuoter +myTagsOnlyHsx = customHsx + (AdditionalTags ["mycustomtag", "anothercustomtag"]) + (AdditionalAttributes []) + +myAttrsOnlyHsx :: QuasiQuoter +myAttrsOnlyHsx = customHsx + (AdditionalTags []) + (AdditionalAttributes ["my-custom-attr", "anothercustomattr"]) \ No newline at end of file diff --git a/ihp-hsx/Test/IHP/HSX/ParserSpec.hs b/ihp-hsx/Test/IHP/HSX/ParserSpec.hs index ad936b7f3..f85622b44 100644 --- a/ihp-hsx/Test/IHP/HSX/ParserSpec.hs +++ b/ihp-hsx/Test/IHP/HSX/ParserSpec.hs @@ -11,134 +11,163 @@ import qualified Text.Megaparsec as Megaparsec import qualified Text.Megaparsec.Error as Megaparsec import qualified "template-haskell" Language.Haskell.TH as TH import qualified "template-haskell" Language.Haskell.TH.Syntax as TH +import qualified Data.Set as Set + tests = do let position = Megaparsec.SourcePos "" (Megaparsec.mkPos 1) (Megaparsec.mkPos 1) let extensions = [] describe "HSX Parser" do - let checkMarkup = True + let settings = HsxSettings True Set.empty Set.empty it "should fail on invalid html tags" do let errorText = "1:13:\n |\n1 | \n | ^\nInvalid tag name: myinvalidel\n" - let (Left error) = parseHsx checkMarkup position extensions "" + let (Left error) = parseHsx settings position extensions "" (Megaparsec.errorBundlePretty error) `shouldBe` errorText it "should fail on invalid attribute names" do let errorText = "1:23:\n |\n1 |
\n | ^\nInvalid attribute name: invalid-attribute\n" - let (Left error) = parseHsx checkMarkup position extensions "
" + let (Left error) = parseHsx settings position extensions "
" (Megaparsec.errorBundlePretty error) `shouldBe` errorText it "should fail on unmatched tags" do let errorText = "1:7:\n |\n1 |
\n | ^\nunexpected '/'\nexpecting \"
\", identifier, or white space\n" - let (Left error) = parseHsx checkMarkup position extensions "
" + let (Left error) = parseHsx settings position extensions "
" (Megaparsec.errorBundlePretty error) `shouldBe` errorText it "should parse a closing tag with spaces" do - let p = parseHsx checkMarkup position extensions "
" + let p = parseHsx settings position extensions "
" p `shouldBe` (Right (Children [Node "div" [] [] False])) it "should strip spaces around nodes" do - let p = parseHsx checkMarkup position extensions "
" + let p = parseHsx settings position extensions "
" p `shouldBe` (Right (Children [Node "div" [] [Node "span" [] [] False] False])) it "should strip spaces after self closing tags" do - let p = parseHsx checkMarkup position extensions "{\"meta\"}\n\n " + let p = parseHsx settings position extensions "{\"meta\"}\n\n " p `shouldBe` (Right (Children [Node "head" [] [SplicedNode (TH.LitE (TH.StringL "meta")),Node "link" [StaticAttribute "rel" (TextValue "stylesheet"),StaticAttribute "href" (TextValue "/vendor/bootstrap.min.css")] [] True] False])) it "should not strip spaces in a text node" do - let p = parseHsx checkMarkup position extensions " Hello World " + let p = parseHsx settings position extensions " Hello World " p `shouldBe` (Right (Children [TextNode "Hello World"])) it "should deal with variables in text nodes" do - let p = parseHsx checkMarkup position extensions "
\n Hello {\"name\"}! \n
" + let p = parseHsx settings position extensions "
\n Hello {\"name\"}! \n
" p `shouldBe` (Right (Children [Node "div" [] [TextNode "Hello ",SplicedNode (TH.LitE (TH.StringL "name")),TextNode "!"] False])) it "should parse self closing tags with spaces around it" do - let p = parseHsx checkMarkup position extensions "
" + let p = parseHsx settings position extensions "
" p `shouldBe` (Right (Children [Node "div" [] [] False])) it "should collapse spaces" do - let p = parseHsx checkMarkup position extensions "\n Hello\n World\n ! " + let p = parseHsx settings position extensions "\n Hello\n World\n ! " p `shouldBe` (Right (Children [TextNode "Hello World !"])) it "should parse spread values" do - let p = parseHsx checkMarkup position extensions "
" + let p = parseHsx settings position extensions "
" -- We cannot easily construct the @VarE variables@ expression, therefore we use show here for comparison show p `shouldBe` "Right (Children [Node \"div\" [SpreadAttributes (VarE variables)] [] False])" it "should parse spread values with a space" do -- See https://github.com/digitallyinduced/ihp/issues/1588 - let p = parseHsx checkMarkup position extensions "
" + let p = parseHsx settings position extensions "
" show p `shouldBe` "Right (Children [Node \"div\" [SpreadAttributes (VarE variables)] [] False])" it "should accept underscores in data attributes" do - let p = parseHsx checkMarkup position extensions "
" + let p = parseHsx settings position extensions "
" p `shouldBe` (Right (Children [Node "div" [StaticAttribute "data-client_id" (TextValue "test")] [] False])) it "should accept doctype" do - let p = parseHsx checkMarkup position extensions "hello" + let p = parseHsx settings position extensions "hello" p `shouldBe` (Right (Children [Node "!DOCTYPE" [StaticAttribute "html" (TextValue "html")] [] True, Node "html" [StaticAttribute "lang" (TextValue "en")] [Node "body" [] [TextNode "hello"] False] False])) describe "uncheckedHsx" do - let checkMarkup = False + let settings = HsxSettings False Set.empty Set.empty it "should not check markup" do - let p = parseHsx False position extensions "" + let p = parseHsx settings position extensions "" p `shouldBe` (Right (Children [Node "invalid-tag" [StaticAttribute "invalid-attribute" (TextValue "invalid")] [] False])) it "should not check attribute names" do - let p = parseHsx False position extensions "
" + let p = parseHsx settings position extensions "
" p `shouldBe` (Right (Children [Node "div" [StaticAttribute "invalid-attribute" (TextValue "invalid")] [] False])) it "should fail on unmatched tags" do let errorText = "1:7:\n |\n1 |
\n | ^\nunexpected '/'\nexpecting \"
\", identifier, or white space\n" - let (Left error) = parseHsx checkMarkup position extensions "
" + let (Left error) = parseHsx settings position extensions "
" (Megaparsec.errorBundlePretty error) `shouldBe` errorText it "should parse a closing tag with spaces" do - let p = parseHsx checkMarkup position extensions "
" + let p = parseHsx settings position extensions "
" p `shouldBe` (Right (Children [Node "div" [] [] False])) it "should strip spaces around nodes" do - let p = parseHsx checkMarkup position extensions "
" + let p = parseHsx settings position extensions "
" p `shouldBe` (Right (Children [Node "div" [] [Node "span" [] [] False] False])) it "should strip spaces after self closing tags" do - let p = parseHsx checkMarkup position extensions "{\"meta\"}\n\n " + let p = parseHsx settings position extensions "{\"meta\"}\n\n " p `shouldBe` (Right (Children [Node "head" [] [SplicedNode (TH.LitE (TH.StringL "meta")),Node "link" [StaticAttribute "rel" (TextValue "stylesheet"),StaticAttribute "href" (TextValue "/vendor/bootstrap.min.css")] [] True] False])) it "should not strip spaces in a text node" do - let p = parseHsx checkMarkup position extensions " Hello World " + let p = parseHsx settings position extensions " Hello World " p `shouldBe` (Right (Children [TextNode "Hello World"])) it "should deal with variables in text nodes" do - let p = parseHsx checkMarkup position extensions "
\n Hello {\"name\"}! \n
" + let p = parseHsx settings position extensions "
\n Hello {\"name\"}! \n
" p `shouldBe` (Right (Children [Node "div" [] [TextNode "Hello ",SplicedNode (TH.LitE (TH.StringL "name")),TextNode "!"] False])) it "should parse self closing tags with spaces around it" do - let p = parseHsx checkMarkup position extensions "
" + let p = parseHsx settings position extensions "
" p `shouldBe` (Right (Children [Node "div" [] [] False])) it "should collapse spaces" do - let p = parseHsx checkMarkup position extensions "\n Hello\n World\n ! " + let p = parseHsx settings position extensions "\n Hello\n World\n ! " p `shouldBe` (Right (Children [TextNode "Hello World !"])) it "should parse spread values" do - let p = parseHsx checkMarkup position extensions "
" + let p = parseHsx settings position extensions "
" -- We cannot easily construct the @VarE variables@ expression, therefore we use show here for comparison show p `shouldBe` "Right (Children [Node \"div\" [SpreadAttributes (VarE variables)] [] False])" it "should parse spread values with a space" do -- See https://github.com/digitallyinduced/ihp/issues/1588 - let p = parseHsx checkMarkup position extensions "
" + let p = parseHsx settings position extensions "
" show p `shouldBe` "Right (Children [Node \"div\" [SpreadAttributes (VarE variables)] [] False])" it "should accept underscores in data attributes" do - let p = parseHsx checkMarkup position extensions "
" + let p = parseHsx settings position extensions "
" p `shouldBe` (Right (Children [Node "div" [StaticAttribute "data-client_id" (TextValue "test")] [] False])) it "should accept doctype" do - let p = parseHsx checkMarkup position extensions "hello" + let p = parseHsx settings position extensions "hello" p `shouldBe` (Right (Children [Node "!DOCTYPE" [StaticAttribute "html" (TextValue "html")] [] True, Node "html" [StaticAttribute "lang" (TextValue "en")] [Node "body" [] [TextNode "hello"] False] False])) + describe "customHsx" do + let customSettings = HsxSettings True + (Set.fromList ["mycustomtag"]) + (Set.fromList ["my-custom-attr"]) - + + it "should allow specified custom tags" do + let p = parseHsx customSettings position extensions "hello" + p `shouldBe` (Right (Children [Node "mycustomtag" [] [TextNode "hello"] False])) + + it "should reject non-specified custom tags" do + let errorText = "1:15:\n |\n1 | hello\n | ^\nInvalid tag name: notallowedtag\n" + case parseHsx customSettings position extensions "hello" of + Left error -> (Megaparsec.errorBundlePretty error) `shouldBe` errorText + Right _ -> fail "Expected parser to fail with invalid tag name" + + it "should allow specified custom attributes" do + let p = parseHsx customSettings position extensions "
test
" + p `shouldBe` (Right (Children [Node "div" [StaticAttribute "my-custom-attr" (TextValue "hello")] [TextNode "test"] False])) + + it "should reject non-specified custom attributes" do + let errorText = "1:22:\n |\n1 |
\n | ^\nInvalid attribute name: not-allowed-attr\n" + case parseHsx customSettings position extensions "
" of + Left error -> (Megaparsec.errorBundlePretty error) `shouldBe` errorText + Right _ -> fail "Expected parser to fail with invalid attribute name" + + it "should allow mixing custom and standard elements" do + let p = parseHsx customSettings position extensions "test" + p `shouldBe` (Right (Children [Node "mycustomtag" [StaticAttribute "class" (TextValue "hello"), StaticAttribute "my-custom-attr" (TextValue "world")] [TextNode "test"] False])) \ No newline at end of file diff --git a/ihp-hsx/Test/IHP/HSX/QQSpec.hs b/ihp-hsx/Test/IHP/HSX/QQSpec.hs index 9bd65c05d..916a71ed3 100644 --- a/ihp-hsx/Test/IHP/HSX/QQSpec.hs +++ b/ihp-hsx/Test/IHP/HSX/QQSpec.hs @@ -10,6 +10,7 @@ import IHP.HSX.QQ import qualified Text.Blaze.Renderer.Text as Blaze import Text.Blaze (preEscapedTextValue) import Data.Text +import IHP.HSX.CustomHsxCases tests :: SpecWith () tests = do @@ -191,6 +192,21 @@ tests = do [hsx|hello|] `shouldBeHtml` "\nhello" + describe "customHsx" do + it "should allow specified custom tags" do + [myTagsOnlyHsx|hello|] `shouldBeHtml` "hello" + [myTagsOnlyHsx|world|] `shouldBeHtml` "world" + + it "should allow specified custom attributes" do + [myAttrsOnlyHsx|
test
|] `shouldBeHtml` "
test
" + [myAttrsOnlyHsx|
test
|] `shouldBeHtml` "
test
" + + it "should allow combining custom tags and attributes" do + [myCustomHsx|test|] `shouldBeHtml` "test" + + it "should work with regular HTML tags and attributes too" do + [myCustomHsx|
world
|] `shouldBeHtml` "
world
" + data Project = Project { name :: Text } data PlaceId = PlaceId Text From e85bb98b0ce18051b7ce54dcba4df4315d5b0ed4 Mon Sep 17 00:00:00 2001 From: lillo Date: Thu, 31 Oct 2024 22:27:32 +0700 Subject: [PATCH 10/29] fix comment --- ihp-hsx/IHP/HSX/Parser.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ihp-hsx/IHP/HSX/Parser.hs b/ihp-hsx/IHP/HSX/Parser.hs index e78fc12bf..ee0a704fa 100644 --- a/ihp-hsx/IHP/HSX/Parser.hs +++ b/ihp-hsx/IHP/HSX/Parser.hs @@ -64,7 +64,7 @@ data Node = Node !Text ![Attribute] ![Node] !Bool -- > let position = Megaparsec.SourcePos filePath (Megaparsec.mkPos line) (Megaparsec.mkPos col) -- > let hsxText = "Hello" -- > --- > let (Right node) = parseHsx True position [] hsxText +-- > let (Right node) = parseHsx settings position extensions hsxText parseHsx :: HsxSettings -> SourcePos -> [TH.Extension] -> Text -> Either (ParseErrorBundle Text Void) Node parseHsx settings position extensions code = let From 23bfdf73650066c26b07abee419dbb94855d2c6b Mon Sep 17 00:00:00 2001 From: lillo Date: Thu, 31 Oct 2024 22:28:20 +0700 Subject: [PATCH 11/29] fix comment --- ihp-hsx/IHP/HSX/Parser.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ihp-hsx/IHP/HSX/Parser.hs b/ihp-hsx/IHP/HSX/Parser.hs index ee0a704fa..512a93821 100644 --- a/ihp-hsx/IHP/HSX/Parser.hs +++ b/ihp-hsx/IHP/HSX/Parser.hs @@ -64,7 +64,7 @@ data Node = Node !Text ![Attribute] ![Node] !Bool -- > let position = Megaparsec.SourcePos filePath (Megaparsec.mkPos line) (Megaparsec.mkPos col) -- > let hsxText = "Hello" -- > --- > let (Right node) = parseHsx settings position extensions hsxText +-- > let (Right node) = parseHsx settings position [] hsxText parseHsx :: HsxSettings -> SourcePos -> [TH.Extension] -> Text -> Either (ParseErrorBundle Text Void) Node parseHsx settings position extensions code = let From b0ebf7916c8ab0f039e3762998ea33f69c3d5c5e Mon Sep 17 00:00:00 2001 From: Lars Lillo Ulvestad Date: Thu, 31 Oct 2024 22:44:26 +0700 Subject: [PATCH 12/29] Update ihp-hsx/IHP/HSX/Parser.hs Co-authored-by: Marc Scholten --- ihp-hsx/IHP/HSX/Parser.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ihp-hsx/IHP/HSX/Parser.hs b/ihp-hsx/IHP/HSX/Parser.hs index 512a93821..565168100 100644 --- a/ihp-hsx/IHP/HSX/Parser.hs +++ b/ihp-hsx/IHP/HSX/Parser.hs @@ -35,11 +35,11 @@ import qualified Data.Set as Set import qualified Data.Containers.ListUtils as List import qualified IHP.HSX.HaskellParser as HaskellParser -data HsxSettings = HsxSettings { - checkMarkup :: Bool, - additionalTagNames :: Set Text, - additionalAttributeNames :: Set Text -} +data HsxSettings = HsxSettings + { checkMarkup :: Bool + , additionalTagNames :: Set Text + , additionalAttributeNames :: Set Text + } data AttributeValue = TextValue !Text | ExpressionValue !Haskell.Exp deriving (Eq, Show) From 7e45f8a72a343a3e140b668b1f07d2cadc317524 Mon Sep 17 00:00:00 2001 From: Lars Lillo Ulvestad Date: Thu, 31 Oct 2024 22:44:39 +0700 Subject: [PATCH 13/29] Update ihp-hsx/IHP/HSX/QQ.hs Co-authored-by: Marc Scholten --- ihp-hsx/IHP/HSX/QQ.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ihp-hsx/IHP/HSX/QQ.hs b/ihp-hsx/IHP/HSX/QQ.hs index 901562f3c..e5306a3c3 100644 --- a/ihp-hsx/IHP/HSX/QQ.hs +++ b/ihp-hsx/IHP/HSX/QQ.hs @@ -39,7 +39,7 @@ hsx = QuasiQuoter { uncheckedHsx :: QuasiQuoter uncheckedHsx = QuasiQuoter { - quoteExp = quoteHsxExpression (HsxSettings False Set.empty Set.empty), + quoteExp = quoteHsxExpression (HsxSettings { checkMarkup = False, additionalTagNames = Set.empty, ... = Set.empty }), quotePat = error "quotePat: not defined", quoteDec = error "quoteDec: not defined", quoteType = error "quoteType: not defined" From ed496737355060b81d585404840ff5b0c6d36ca2 Mon Sep 17 00:00:00 2001 From: lillo Date: Thu, 31 Oct 2024 23:09:38 +0700 Subject: [PATCH 14/29] remove newtypes and use 'HsxSettings' directly --- ihp-hsx/IHP/HSX/QQ.hs | 11 ++++------- ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs | 22 ++++++++++++++++------ ihp-hsx/Test/IHP/HSX/ParserSpec.hs | 1 - 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/ihp-hsx/IHP/HSX/QQ.hs b/ihp-hsx/IHP/HSX/QQ.hs index e5306a3c3..d595c7912 100644 --- a/ihp-hsx/IHP/HSX/QQ.hs +++ b/ihp-hsx/IHP/HSX/QQ.hs @@ -5,7 +5,7 @@ Module: IHP.HSX.QQ Description: Defines the @[hsx||]@ syntax Copyright: (c) digitally induced GmbH, 2022 -} -module IHP.HSX.QQ (hsx, uncheckedHsx, customHsx, AdditionalTags(..), AdditionalAttributes(..)) where +module IHP.HSX.QQ (hsx, uncheckedHsx, customHsx) where import Prelude import Data.Text (Text) @@ -45,12 +45,9 @@ uncheckedHsx = QuasiQuoter { quoteType = error "quoteType: not defined" } -newtype AdditionalTags = AdditionalTags [Text] -newtype AdditionalAttributes = AdditionalAttributes [Text] - -customHsx :: AdditionalTags -> AdditionalAttributes -> QuasiQuoter -customHsx (AdditionalTags additionalTagNames) (AdditionalAttributes additionalAttributeNames) = QuasiQuoter { - quoteExp = quoteHsxExpression (HsxSettings True (Set.fromList additionalTagNames) (Set.fromList additionalAttributeNames)), +customHsx :: HsxSettings -> QuasiQuoter +customHsx settings = QuasiQuoter { + quoteExp = quoteHsxExpression settings, quotePat = error "quotePat: not defined", quoteDec = error "quoteDec: not defined", quoteType = error "quoteType: not defined" diff --git a/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs b/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs index 4763b6039..81be7ba2c 100644 --- a/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs +++ b/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs @@ -11,18 +11,28 @@ import qualified Text.Blaze.Renderer.Text as Blaze import Data.Text import Language.Haskell.TH.Quote import IHP.HSX.Parser +import qualified Data.Set as Set myCustomHsx :: QuasiQuoter myCustomHsx = customHsx - (AdditionalTags ["mycustomtag", "anothercustomtag"]) - (AdditionalAttributes ["my-custom-attr", "anothercustomattr"]) + (HsxSettings { checkMarkup = True + , additionalTagNames = Set.fromList ["mycustomtag", "anothercustomtag"] + , additionalAttributeNames = Set.fromList ["my-custom-attr", "anothercustomattr"] + } + ) myTagsOnlyHsx :: QuasiQuoter myTagsOnlyHsx = customHsx - (AdditionalTags ["mycustomtag", "anothercustomtag"]) - (AdditionalAttributes []) + (HsxSettings { checkMarkup = True + , additionalTagNames = Set.fromList ["mycustomtag", "anothercustomtag"] + , additionalAttributeNames = Set.fromList [] + } + ) myAttrsOnlyHsx :: QuasiQuoter myAttrsOnlyHsx = customHsx - (AdditionalTags []) - (AdditionalAttributes ["my-custom-attr", "anothercustomattr"]) \ No newline at end of file + (HsxSettings { checkMarkup = True + , additionalTagNames = Set.fromList [] + , additionalAttributeNames = Set.fromList ["my-custom-attr", "anothercustomattr"] + } + ) diff --git a/ihp-hsx/Test/IHP/HSX/ParserSpec.hs b/ihp-hsx/Test/IHP/HSX/ParserSpec.hs index f85622b44..8087828a5 100644 --- a/ihp-hsx/Test/IHP/HSX/ParserSpec.hs +++ b/ihp-hsx/Test/IHP/HSX/ParserSpec.hs @@ -147,7 +147,6 @@ tests = do (Set.fromList ["mycustomtag"]) (Set.fromList ["my-custom-attr"]) - it "should allow specified custom tags" do let p = parseHsx customSettings position extensions "hello" p `shouldBe` (Right (Children [Node "mycustomtag" [] [TextNode "hello"] False])) From 3a7bd62bc0ee9ab9fcc4a971b5c74d95155f65c4 Mon Sep 17 00:00:00 2001 From: lillo Date: Thu, 31 Oct 2024 23:15:54 +0700 Subject: [PATCH 15/29] Fix Github resolve bug --- ihp-hsx/IHP/HSX/QQ.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ihp-hsx/IHP/HSX/QQ.hs b/ihp-hsx/IHP/HSX/QQ.hs index d595c7912..dfa72be15 100644 --- a/ihp-hsx/IHP/HSX/QQ.hs +++ b/ihp-hsx/IHP/HSX/QQ.hs @@ -31,7 +31,7 @@ import qualified Data.Set as Set hsx :: QuasiQuoter hsx = QuasiQuoter { - quoteExp = quoteHsxExpression (HsxSettings True Set.empty Set.empty), + quoteExp = quoteHsxExpression (HsxSettings { checkMarkup = True, additionalTagNames = Set.empty, additionalAttributeNames = Set.empty }), quotePat = error "quotePat: not defined", quoteDec = error "quoteDec: not defined", quoteType = error "quoteType: not defined" @@ -39,7 +39,7 @@ hsx = QuasiQuoter { uncheckedHsx :: QuasiQuoter uncheckedHsx = QuasiQuoter { - quoteExp = quoteHsxExpression (HsxSettings { checkMarkup = False, additionalTagNames = Set.empty, ... = Set.empty }), + quoteExp = quoteHsxExpression (HsxSettings { checkMarkup = False, additionalTagNames = Set.empty, additionalAttributeNames = Set.empty }), quotePat = error "quotePat: not defined", quoteDec = error "quoteDec: not defined", quoteType = error "quoteType: not defined" From 6eb7c17c207877daa582f60dd93f48fd90ce1e59 Mon Sep 17 00:00:00 2001 From: lillo Date: Fri, 1 Nov 2024 00:51:06 +0700 Subject: [PATCH 16/29] Aesthethic nitpick --- ihp-hsx/IHP/HSX/Parser.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ihp-hsx/IHP/HSX/Parser.hs b/ihp-hsx/IHP/HSX/Parser.hs index 565168100..2c1eac21e 100644 --- a/ihp-hsx/IHP/HSX/Parser.hs +++ b/ihp-hsx/IHP/HSX/Parser.hs @@ -219,9 +219,8 @@ hsxNodeAttribute = do hsxAttributeName :: Parser Text hsxAttributeName = do name <- rawAttribute - let shouldCheckMarkup = ?settings.checkMarkup - let isValidAdditionalAttribute = name `Set.member` ?settings.additionalAttributeNames - unless (isValidAttributeName name || not shouldCheckMarkup || isValidAdditionalAttribute) (fail $ "Invalid attribute name: " <> cs name) + let checkingMarkup = ?settings.checkMarkup + unless (isValidAttributeName name || not checkingMarkup) (fail $ "Invalid attribute name: " <> cs name) pure name where isValidAttributeName name = @@ -229,6 +228,7 @@ hsxAttributeName = do || "aria-" `Text.isPrefixOf` name || "hx-" `Text.isPrefixOf` name || name `Set.member` attributes + || name `Set.member` ?settings.additionalAttributeNames rawAttribute = takeWhile1P Nothing (\c -> Char.isAlphaNum c || c == '-' || c == '_') @@ -304,8 +304,8 @@ hsxElementName = do && not (Text.isPrefixOf "-" name) && not (Char.isNumber (Text.head name)) let isValidAdditionalTag = name `Set.member` ?settings.additionalTagNames - let shouldCheckMarkup = ?settings.checkMarkup - unless (isValidParent || isValidLeaf || isValidCustomWebComponent || isValidAdditionalTag || not shouldCheckMarkup) (fail $ "Invalid tag name: " <> cs name) + let checkingMarkup = ?settings.checkMarkup + unless (isValidParent || isValidLeaf || isValidCustomWebComponent || isValidAdditionalTag || not checkingMarkup) (fail $ "Invalid tag name: " <> cs name) space pure name From 83c3d2f5e14019369970bb827199b913ca69c9ea Mon Sep 17 00:00:00 2001 From: lillo Date: Fri, 1 Nov 2024 19:34:49 +0700 Subject: [PATCH 17/29] use customHsx to call hsx and uncheckedHsx --- ihp-hsx/IHP/HSX/QQ.hs | 26 ++++++++++++++------------ ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/ihp-hsx/IHP/HSX/QQ.hs b/ihp-hsx/IHP/HSX/QQ.hs index dfa72be15..a8aefb5f4 100644 --- a/ihp-hsx/IHP/HSX/QQ.hs +++ b/ihp-hsx/IHP/HSX/QQ.hs @@ -30,20 +30,22 @@ import qualified Data.HashMap.Strict as HashMap import qualified Data.Set as Set hsx :: QuasiQuoter -hsx = QuasiQuoter { - quoteExp = quoteHsxExpression (HsxSettings { checkMarkup = True, additionalTagNames = Set.empty, additionalAttributeNames = Set.empty }), - quotePat = error "quotePat: not defined", - quoteDec = error "quoteDec: not defined", - quoteType = error "quoteType: not defined" - } +hsx = customHsx + (HsxSettings + { checkMarkup = True + , additionalTagNames = Set.empty + , additionalAttributeNames = Set.empty + } + ) uncheckedHsx :: QuasiQuoter -uncheckedHsx = QuasiQuoter { - quoteExp = quoteHsxExpression (HsxSettings { checkMarkup = False, additionalTagNames = Set.empty, additionalAttributeNames = Set.empty }), - quotePat = error "quotePat: not defined", - quoteDec = error "quoteDec: not defined", - quoteType = error "quoteType: not defined" - } +uncheckedHsx = customHsx + (HsxSettings + { checkMarkup = False + , additionalTagNames = Set.empty + , additionalAttributeNames = Set.empty + } + ) customHsx :: HsxSettings -> QuasiQuoter customHsx settings = QuasiQuoter { diff --git a/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs b/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs index 81be7ba2c..3a4a49590 100644 --- a/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs +++ b/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs @@ -2,7 +2,7 @@ Module: IHP.HSX.TestHsx Description: Test helpers for HSX tests with custom tags and attributes -} -module IHP.HSX.CustomHsxCases where +module Test.IHP.HSX.CustomHsxCases where import Test.Hspec import Prelude From 0ad0581794d23a6766742c4a36aa4ebf59549f0d Mon Sep 17 00:00:00 2001 From: lillo Date: Fri, 1 Nov 2024 20:30:19 +0700 Subject: [PATCH 18/29] Move CustomHsxCases to Test.HSX namespace --- ihp-hsx/Test/{IHP => }/HSX/CustomHsxCases.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename ihp-hsx/Test/{IHP => }/HSX/CustomHsxCases.hs (96%) diff --git a/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs b/ihp-hsx/Test/HSX/CustomHsxCases.hs similarity index 96% rename from ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs rename to ihp-hsx/Test/HSX/CustomHsxCases.hs index 3a4a49590..330e809ae 100644 --- a/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs +++ b/ihp-hsx/Test/HSX/CustomHsxCases.hs @@ -2,7 +2,7 @@ Module: IHP.HSX.TestHsx Description: Test helpers for HSX tests with custom tags and attributes -} -module Test.IHP.HSX.CustomHsxCases where +module Test.HSX.CustomHsxCases where import Test.Hspec import Prelude From a64d6ef84bda4998c8dc8e77c6f074390fb4f5f4 Mon Sep 17 00:00:00 2001 From: lillo Date: Fri, 1 Nov 2024 20:39:31 +0700 Subject: [PATCH 19/29] Fix import --- ihp-hsx/Test/IHP/HSX/QQSpec.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ihp-hsx/Test/IHP/HSX/QQSpec.hs b/ihp-hsx/Test/IHP/HSX/QQSpec.hs index 916a71ed3..1912d709b 100644 --- a/ihp-hsx/Test/IHP/HSX/QQSpec.hs +++ b/ihp-hsx/Test/IHP/HSX/QQSpec.hs @@ -10,7 +10,7 @@ import IHP.HSX.QQ import qualified Text.Blaze.Renderer.Text as Blaze import Text.Blaze (preEscapedTextValue) import Data.Text -import IHP.HSX.CustomHsxCases +import Test.HSX.CustomHsxCases tests :: SpecWith () tests = do From 2ff26926b0ecefaa8980335e50b3df7130115050 Mon Sep 17 00:00:00 2001 From: lillo Date: Fri, 1 Nov 2024 20:41:57 +0700 Subject: [PATCH 20/29] Fix module comment --- ihp-hsx/Test/HSX/CustomHsxCases.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ihp-hsx/Test/HSX/CustomHsxCases.hs b/ihp-hsx/Test/HSX/CustomHsxCases.hs index 330e809ae..fa62a11c2 100644 --- a/ihp-hsx/Test/HSX/CustomHsxCases.hs +++ b/ihp-hsx/Test/HSX/CustomHsxCases.hs @@ -1,5 +1,5 @@ {-| -Module: IHP.HSX.TestHsx +Module: Test.HSX.CustomHsxCases Description: Test helpers for HSX tests with custom tags and attributes -} module Test.HSX.CustomHsxCases where From 0185bea6f7d2161769d40d93b1955ae5b9740c9e Mon Sep 17 00:00:00 2001 From: lillo Date: Fri, 1 Nov 2024 21:48:08 +0700 Subject: [PATCH 21/29] For now, move CustomHsxCases back so the tests are working again --- ihp-hsx/Test/{ => IHP}/HSX/CustomHsxCases.hs | 2 +- ihp-hsx/Test/IHP/HSX/QQSpec.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename ihp-hsx/Test/{ => IHP}/HSX/CustomHsxCases.hs (96%) diff --git a/ihp-hsx/Test/HSX/CustomHsxCases.hs b/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs similarity index 96% rename from ihp-hsx/Test/HSX/CustomHsxCases.hs rename to ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs index fa62a11c2..af39fcaf2 100644 --- a/ihp-hsx/Test/HSX/CustomHsxCases.hs +++ b/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs @@ -2,7 +2,7 @@ Module: Test.HSX.CustomHsxCases Description: Test helpers for HSX tests with custom tags and attributes -} -module Test.HSX.CustomHsxCases where +module IHP.HSX.CustomHsxCases where import Test.Hspec import Prelude diff --git a/ihp-hsx/Test/IHP/HSX/QQSpec.hs b/ihp-hsx/Test/IHP/HSX/QQSpec.hs index 1912d709b..916a71ed3 100644 --- a/ihp-hsx/Test/IHP/HSX/QQSpec.hs +++ b/ihp-hsx/Test/IHP/HSX/QQSpec.hs @@ -10,7 +10,7 @@ import IHP.HSX.QQ import qualified Text.Blaze.Renderer.Text as Blaze import Text.Blaze (preEscapedTextValue) import Data.Text -import Test.HSX.CustomHsxCases +import IHP.HSX.CustomHsxCases tests :: SpecWith () tests = do From 086497982b4f401aaee41b1e2e2788298431e569 Mon Sep 17 00:00:00 2001 From: lillo Date: Fri, 1 Nov 2024 23:52:14 +0700 Subject: [PATCH 22/29] Add documentation --- Guide/hsx.markdown | 87 +++++++++++++++++++++++++++++++++++++++++++ ihp-hsx/IHP/HSX/QQ.hs | 15 ++++---- ihp-hsx/README.md | 87 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 7 deletions(-) diff --git a/Guide/hsx.markdown b/Guide/hsx.markdown index f58a933d1..c96ee5a6e 100644 --- a/Guide/hsx.markdown +++ b/Guide/hsx.markdown @@ -278,6 +278,93 @@ The underlying HTML library blaze currently does not support an empty HTML attri If you use HTML entities, such as ` ` for a non-breaking space, you will notice they appear exactly like that. To output directly (i.e. unescaped) use the method `preEscapedToMarkup` from `Text.Blaze.Html5`. +### Custom HSX and Unchecked HSX + +HSX provides two additional QuasiQuoters beyond the standard `[hsx|...|]` for increased flexibility: `uncheckedHsx` and `customHsx`. + +#### Using `uncheckedHsx` + +`uncheckedHsx` provides a quick way to bypass HSX's strict tag and attribute name checking. + +It will still check for valid HTML, but it will accept any tag and attribute names. + + +```haskell +[uncheckedHsx| + + Content + +|] +``` + +While convenient for rapid development, use it with caution as you lose the benefits of compile-time guarantees for your markup. + +#### Using `customHsx` + +`customHsx` allows you to extend the default HSX with additional whitelisted tag names and attribute names while maintaining the same strict compile-time checking of the default `hsx`. + +This makes it easier to use custom elements that often also contain special attributes, and javascript libraries, for example `_hyperscript`, that use the `_` as an attribute name. + + +To use `customHsx`, you need to create it in a separate module due to Template Haskell restrictions. Here's how to set it up: + +1. First, create a new module for your custom HSX (e.g., `Application.Helper.CustomHsx`): + +```haskell +module Application.Helper.CustomHsx where + +import IHP.Prelude +import IHP.HSX.QQ (customHsx) +import IHP.HSX.Parser +import Language.Haskell.TH.Quote +import qualified Data.Set as Set + +myHsx :: QuasiQuoter +myHsx = customHsx + (HsxSettings + { checkMarkup = True + , additionalTagNames = Set.fromList ["book", "title", "name"] + , additionalAttributeNames = Set.fromList ["_", "custom-attribute"] + } + ) +``` + +Configuration options for `HsxSettings`: +- `checkMarkup`: Boolean to enable/disable markup checking +- `additionalTagNames`: Set of additional allowed tag names +- `additionalAttributeNames`: Set of additional allowed attribute names + +2. Make it available in your views by adding it to your view helpers module: + +```haskell +module Application.Helper.View ( + module Application.Helper.View, + module Application.Helper.CustomHsx -- Add this line +) where + +import IHP.ViewPrelude +import Application.Helper.CustomHsx (myHsx) -- Add this line +``` + +3. Use it in your views: + +```haskell +[myHsx| + + My Book + Author Name + +|] +``` + +The custom HSX will validate that tags and attributes are either in the default HSX whitelist or in your additional sets. This gives you the flexibility to use custom elements and attributes. + +This approach is particularly useful for: +- Web Components with custom attribute names +- UI libraries with non-standard attributes +- Integration with third-party frameworks that extend HTML syntax + +It's not usable for libraries with highly esoteric attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. ## Common HSX Patterns diff --git a/ihp-hsx/IHP/HSX/QQ.hs b/ihp-hsx/IHP/HSX/QQ.hs index a8aefb5f4..4599c7ff2 100644 --- a/ihp-hsx/IHP/HSX/QQ.hs +++ b/ihp-hsx/IHP/HSX/QQ.hs @@ -45,15 +45,16 @@ uncheckedHsx = customHsx , additionalTagNames = Set.empty , additionalAttributeNames = Set.empty } - ) + ) customHsx :: HsxSettings -> QuasiQuoter -customHsx settings = QuasiQuoter { - quoteExp = quoteHsxExpression settings, - quotePat = error "quotePat: not defined", - quoteDec = error "quoteDec: not defined", - quoteType = error "quoteType: not defined" - } +customHsx settings = + QuasiQuoter + { quoteExp = quoteHsxExpression settings + , quotePat = error "quotePat: not defined" + , quoteDec = error "quoteDec: not defined" + , quoteType = error "quoteType: not defined" + } quoteHsxExpression :: HsxSettings -> String -> TH.ExpQ quoteHsxExpression settings code = do diff --git a/ihp-hsx/README.md b/ihp-hsx/README.md index 20fcc3a4f..68b625766 100644 --- a/ihp-hsx/README.md +++ b/ihp-hsx/README.md @@ -274,6 +274,93 @@ The underlying HTML library blaze currently does not support an empty HTML attri If you use HTML entities, such as ` ` for a non-breaking space, you will notice they appear exactly like that. To output directly (i.e. unescaped) use the method `preEscapedToMarkup` from `Text.Blaze.Html5`. +### Custom HSX and Unchecked HSX + +HSX provides two additional QuasiQuoters beyond the standard `[hsx|...|]` for increased flexibility: `uncheckedHsx` and `customHsx`. + +#### Using `uncheckedHsx` + +`uncheckedHsx` provides a quick way to bypass HSX's strict tag and attribute name checking. + +It will still check for valid HTML, but it will accept any tag and attribute names. + + +```haskell +[uncheckedHsx| + + Content + +|] +``` + +While convenient for rapid development, use it with caution as you lose the benefits of compile-time guarantees for your markup. + +#### Using `customHsx` + +`customHsx` allows you to extend the default HSX with additional whitelisted tag names and attribute names while maintaining the same strict compile-time checking of the default `hsx`. + +This makes it easier to use custom elements that often also contain special attributes, and javascript libraries, for example `_hyperscript`, that use the `_` as an attribute name. + + +To use `customHsx`, you need to create it in a separate module due to Template Haskell restrictions. Here's how to set it up: + +1. First, create a new module for your custom HSX (e.g., `Application.Helper.CustomHsx`): + +```haskell +module Application.Helper.CustomHsx where + +import IHP.Prelude +import IHP.HSX.QQ (customHsx) +import IHP.HSX.Parser +import Language.Haskell.TH.Quote +import qualified Data.Set as Set + +myHsx :: QuasiQuoter +myHsx = customHsx + (HsxSettings + { checkMarkup = True + , additionalTagNames = Set.fromList ["book", "title", "name"] + , additionalAttributeNames = Set.fromList ["_", "custom-attribute"] + } + ) +``` + +Configuration options for `HsxSettings`: +- `checkMarkup`: Boolean to enable/disable markup checking +- `additionalTagNames`: Set of additional allowed tag names +- `additionalAttributeNames`: Set of additional allowed attribute names + +2. Make it available in your views by adding it to your view helpers module: + +```haskell +module Application.Helper.View ( + module Application.Helper.View, + module Application.Helper.CustomHsx -- Add this line +) where + +import IHP.ViewPrelude +import Application.Helper.CustomHsx (myHsx) -- Add this line +``` + +3. Use it in your views: + +```haskell +[myHsx| + + My Book + Author Name + +|] +``` + +The custom HSX will validate that tags and attributes are either in the default HSX whitelist or in your additional sets. This gives you the flexibility to use custom elements and attributes. + +This approach is particularly useful for: +- Web Components with custom attribute names +- UI libraries with non-standard attributes +- Integration with third-party frameworks that extend HTML syntax + +It's not usable for libraries with highly esoteric attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. ## Common HSX Patterns From 1de5cb8a1a9ebbb4cca0d9b7079279e9850e5806 Mon Sep 17 00:00:00 2001 From: lillo Date: Fri, 1 Nov 2024 23:55:57 +0700 Subject: [PATCH 23/29] Minor doc fix --- Guide/hsx.markdown | 2 +- ihp-hsx/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Guide/hsx.markdown b/Guide/hsx.markdown index c96ee5a6e..513bc01cd 100644 --- a/Guide/hsx.markdown +++ b/Guide/hsx.markdown @@ -286,7 +286,7 @@ HSX provides two additional QuasiQuoters beyond the standard `[hsx|...|]` for in `uncheckedHsx` provides a quick way to bypass HSX's strict tag and attribute name checking. -It will still check for valid HTML, but it will accept any tag and attribute names. +It will still check for a valid HTML structure, but it will accept any tag and attribute names. ```haskell diff --git a/ihp-hsx/README.md b/ihp-hsx/README.md index 68b625766..8d5e5e33e 100644 --- a/ihp-hsx/README.md +++ b/ihp-hsx/README.md @@ -282,7 +282,7 @@ HSX provides two additional QuasiQuoters beyond the standard `[hsx|...|]` for in `uncheckedHsx` provides a quick way to bypass HSX's strict tag and attribute name checking. -It will still check for valid HTML, but it will accept any tag and attribute names. +It will still check for a valid HTML structure, but it will accept any tag and attribute names. ```haskell From 610ae2f88321599cda0bf40bef0e63800dbbc529 Mon Sep 17 00:00:00 2001 From: lillo Date: Sat, 2 Nov 2024 00:03:58 +0700 Subject: [PATCH 24/29] Formulate solution to QuasiQuoter shortcomings --- Guide/hsx.markdown | 2 +- ihp-hsx/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Guide/hsx.markdown b/Guide/hsx.markdown index 513bc01cd..9e8cdf696 100644 --- a/Guide/hsx.markdown +++ b/Guide/hsx.markdown @@ -364,7 +364,7 @@ This approach is particularly useful for: - UI libraries with non-standard attributes - Integration with third-party frameworks that extend HTML syntax -It's not usable for libraries with highly esoteric attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. +It's not usable for libraries with highly esoteric attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` likely your best bet. ## Common HSX Patterns diff --git a/ihp-hsx/README.md b/ihp-hsx/README.md index 8d5e5e33e..d3e93de3d 100644 --- a/ihp-hsx/README.md +++ b/ihp-hsx/README.md @@ -360,7 +360,7 @@ This approach is particularly useful for: - UI libraries with non-standard attributes - Integration with third-party frameworks that extend HTML syntax -It's not usable for libraries with highly esoteric attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. +It's not usable for libraries with highly esoteric attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` likely your best bet. ## Common HSX Patterns From 502aa2d9475aea386aa56202b53e53575e4a7df9 Mon Sep 17 00:00:00 2001 From: lillo Date: Sat, 2 Nov 2024 00:04:36 +0700 Subject: [PATCH 25/29] typo fix --- Guide/hsx.markdown | 2 +- ihp-hsx/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Guide/hsx.markdown b/Guide/hsx.markdown index 9e8cdf696..9fbeecc9b 100644 --- a/Guide/hsx.markdown +++ b/Guide/hsx.markdown @@ -364,7 +364,7 @@ This approach is particularly useful for: - UI libraries with non-standard attributes - Integration with third-party frameworks that extend HTML syntax -It's not usable for libraries with highly esoteric attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` likely your best bet. +It's not usable for libraries with highly esoteric attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` is likely your best bet. ## Common HSX Patterns diff --git a/ihp-hsx/README.md b/ihp-hsx/README.md index d3e93de3d..3a4d08b7a 100644 --- a/ihp-hsx/README.md +++ b/ihp-hsx/README.md @@ -360,7 +360,7 @@ This approach is particularly useful for: - UI libraries with non-standard attributes - Integration with third-party frameworks that extend HTML syntax -It's not usable for libraries with highly esoteric attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` likely your best bet. +It's not usable for libraries with highly esoteric attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` is likely your best bet. ## Common HSX Patterns From baafd7c17cbf08447034d6ec33066445165976c0 Mon Sep 17 00:00:00 2001 From: lillo Date: Sat, 2 Nov 2024 00:07:03 +0700 Subject: [PATCH 26/29] Add use-case example --- Guide/hsx.markdown | 1 + ihp-hsx/README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/Guide/hsx.markdown b/Guide/hsx.markdown index 9fbeecc9b..df55bc912 100644 --- a/Guide/hsx.markdown +++ b/Guide/hsx.markdown @@ -362,6 +362,7 @@ The custom HSX will validate that tags and attributes are either in the default This approach is particularly useful for: - Web Components with custom attribute names - UI libraries with non-standard attributes +- Domain-specific XML markup languages like [Hyperview](https://hyperview.org/docs/example_navigation) - Integration with third-party frameworks that extend HTML syntax It's not usable for libraries with highly esoteric attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` is likely your best bet. diff --git a/ihp-hsx/README.md b/ihp-hsx/README.md index 3a4d08b7a..bc2151d70 100644 --- a/ihp-hsx/README.md +++ b/ihp-hsx/README.md @@ -358,6 +358,7 @@ The custom HSX will validate that tags and attributes are either in the default This approach is particularly useful for: - Web Components with custom attribute names - UI libraries with non-standard attributes +- Domain-specific XML markup languages - Integration with third-party frameworks that extend HTML syntax It's not usable for libraries with highly esoteric attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` is likely your best bet. From 035ee284f5964ea0d49d584a179f884c5ac696b7 Mon Sep 17 00:00:00 2001 From: lillo Date: Sat, 2 Nov 2024 00:14:25 +0700 Subject: [PATCH 27/29] Simplify langauge --- Guide/hsx.markdown | 2 +- ihp-hsx/README.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Guide/hsx.markdown b/Guide/hsx.markdown index df55bc912..6139de8c6 100644 --- a/Guide/hsx.markdown +++ b/Guide/hsx.markdown @@ -365,7 +365,7 @@ This approach is particularly useful for: - Domain-specific XML markup languages like [Hyperview](https://hyperview.org/docs/example_navigation) - Integration with third-party frameworks that extend HTML syntax -It's not usable for libraries with highly esoteric attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` is likely your best bet. +It's not usable for libraries with very unusual symbols in their attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` is likely your best bet. ## Common HSX Patterns diff --git a/ihp-hsx/README.md b/ihp-hsx/README.md index bc2151d70..85068d62b 100644 --- a/ihp-hsx/README.md +++ b/ihp-hsx/README.md @@ -361,7 +361,8 @@ This approach is particularly useful for: - Domain-specific XML markup languages - Integration with third-party frameworks that extend HTML syntax -It's not usable for libraries with highly esoteric attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` is likely your best bet. +It's not usable for libraries with very unusual symbols in their attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` is likely your best bet. + ## Common HSX Patterns From e7522e3b4ac05ec83cec164261445b04ec68dd85 Mon Sep 17 00:00:00 2001 From: lillo Date: Sat, 2 Nov 2024 03:06:00 +0700 Subject: [PATCH 28/29] Improve examples a bit --- Guide/hsx.markdown | 10 +++++----- ihp-hsx/README.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Guide/hsx.markdown b/Guide/hsx.markdown index 6139de8c6..d9680ce65 100644 --- a/Guide/hsx.markdown +++ b/Guide/hsx.markdown @@ -291,9 +291,9 @@ It will still check for a valid HTML structure, but it will accept any tag and a ```haskell [uncheckedHsx| - + Content - + |] ``` @@ -323,7 +323,7 @@ myHsx :: QuasiQuoter myHsx = customHsx (HsxSettings { checkMarkup = True - , additionalTagNames = Set.fromList ["book", "title", "name"] + , additionalTagNames = Set.fromList ["book", "heading", "name"] , additionalAttributeNames = Set.fromList ["_", "custom-attribute"] } ) @@ -350,8 +350,8 @@ import Application.Helper.CustomHsx (myHsx) -- Add this line ```haskell [myHsx| - - My Book + + My Book Author Name |] diff --git a/ihp-hsx/README.md b/ihp-hsx/README.md index 85068d62b..18d86192d 100644 --- a/ihp-hsx/README.md +++ b/ihp-hsx/README.md @@ -287,9 +287,9 @@ It will still check for a valid HTML structure, but it will accept any tag and a ```haskell [uncheckedHsx| - + Content - + |] ``` @@ -319,7 +319,7 @@ myHsx :: QuasiQuoter myHsx = customHsx (HsxSettings { checkMarkup = True - , additionalTagNames = Set.fromList ["book", "title", "name"] + , additionalTagNames = Set.fromList ["book", "heading", "name"] , additionalAttributeNames = Set.fromList ["_", "custom-attribute"] } ) @@ -346,8 +346,8 @@ import Application.Helper.CustomHsx (myHsx) -- Add this line ```haskell [myHsx| - - My Book + + My Book Author Name |] From 5c1475944377c49b83afa53deda692f483311e05 Mon Sep 17 00:00:00 2001 From: lillo Date: Sat, 2 Nov 2024 10:33:55 +0700 Subject: [PATCH 29/29] Add spread example --- Guide/hsx.markdown | 10 +++++++++- ihp-hsx/README.md | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Guide/hsx.markdown b/Guide/hsx.markdown index d9680ce65..4fe57a38c 100644 --- a/Guide/hsx.markdown +++ b/Guide/hsx.markdown @@ -365,7 +365,15 @@ This approach is particularly useful for: - Domain-specific XML markup languages like [Hyperview](https://hyperview.org/docs/example_navigation) - Integration with third-party frameworks that extend HTML syntax -It's not usable for libraries with very unusual symbols in their attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` is likely your best bet. +`customHsx` whitelisting and even `uncheckedHsx` does not entirely help for libraries with very unusual symbols in their attributes, like Alpine.js, because they don't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` is likely your best bet. + +```haskell +-- This will not work +[uncheckedHsx||] + +-- Using spread syntax will work +[hsx||] +``` ## Common HSX Patterns diff --git a/ihp-hsx/README.md b/ihp-hsx/README.md index 18d86192d..d576bbbc2 100644 --- a/ihp-hsx/README.md +++ b/ihp-hsx/README.md @@ -361,8 +361,15 @@ This approach is particularly useful for: - Domain-specific XML markup languages - Integration with third-party frameworks that extend HTML syntax -It's not usable for libraries with very unusual symbols in their attributes, like Alpine.js. Even `uncheckedHsx` will throw an error because it doesn't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` is likely your best bet. +`customHsx` whitelisting and even `uncheckedHsx` does not entirely help for libraries with very unusual symbols in their attributes, like Alpine.js, because they don't recognize html attributes starting with `@` or has `:` in the attribute name. In these cases, the spread syntax `{...attributeList}` is likely your best bet. +```haskell +-- This will not work +[uncheckedHsx||] + +-- Using spread syntax will work +[hsx||] +``` ## Common HSX Patterns