diff --git a/Guide/hsx.markdown b/Guide/hsx.markdown
index f58a933d1..4fe57a38c 100644
--- a/Guide/hsx.markdown
+++ b/Guide/hsx.markdown
@@ -278,6 +278,102 @@ 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 a valid HTML structure, 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", "heading", "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
+- Domain-specific XML markup languages like [Hyperview](https://hyperview.org/docs/example_navigation)
+- Integration with third-party frameworks that extend HTML syntax
+
+`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/IHP/HSX/Parser.hs b/ihp-hsx/IHP/HSX/Parser.hs
index 1f50d1d5c..2c1eac21e 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)
@@ -57,15 +64,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 settings position [] hsxText
+parseHsx :: HsxSettings -> SourcePos -> [TH.Extension] -> Text -> Either (ParseErrorBundle Text Void) Node
+parseHsx settings position extensions code =
let
?extensions = extensions
+ ?settings = settings
in
runParser (setPosition position *> parser) "" code
-type Parser a = (?extensions :: [TH.Extension]) => 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 }
@@ -211,15 +219,16 @@ hsxNodeAttribute = do
hsxAttributeName :: Parser Text
hsxAttributeName = do
name <- rawAttribute
- unless (isValidAttributeName name) (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 =
"data-" `Text.isPrefixOf` name
|| "aria-" `Text.isPrefixOf` name
|| "hx-" `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 == '_')
@@ -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) (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 checkingMarkup = ?settings.checkMarkup
+ unless (isValidParent || isValidLeaf || isValidCustomWebComponent || isValidAdditionalTag || not checkingMarkup) (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 a064f2fb4..4599c7ff2 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, customHsx) where
import Prelude
import Data.Text (Text)
@@ -27,20 +27,40 @@ 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,
- 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
+ }
+ )
-quoteHsxExpression :: String -> TH.ExpQ
-quoteHsxExpression code = do
+uncheckedHsx :: QuasiQuoter
+uncheckedHsx = customHsx
+ (HsxSettings
+ { checkMarkup = False
+ , 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"
+ }
+
+quoteHsxExpression :: HsxSettings -> String -> TH.ExpQ
+quoteHsxExpression settings code = do
hsxPosition <- findHSXPosition
extensions <- TH.extsEnabled
- expression <- case parseHsx 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
@@ -458,4 +478,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/README.md b/ihp-hsx/README.md
index 20fcc3a4f..d576bbbc2 100644
--- a/ihp-hsx/README.md
+++ b/ihp-hsx/README.md
@@ -274,6 +274,102 @@ 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 a valid HTML structure, 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", "heading", "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
+- Domain-specific XML markup languages
+- Integration with third-party frameworks that extend HTML syntax
+
+`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/Test/IHP/HSX/CustomHsxCases.hs b/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs
new file mode 100644
index 000000000..af39fcaf2
--- /dev/null
+++ b/ihp-hsx/Test/IHP/HSX/CustomHsxCases.hs
@@ -0,0 +1,38 @@
+{-|
+Module: Test.HSX.CustomHsxCases
+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
+import qualified Data.Set as Set
+
+myCustomHsx :: QuasiQuoter
+myCustomHsx = customHsx
+ (HsxSettings { checkMarkup = True
+ , additionalTagNames = Set.fromList ["mycustomtag", "anothercustomtag"]
+ , additionalAttributeNames = Set.fromList ["my-custom-attr", "anothercustomattr"]
+ }
+ )
+
+myTagsOnlyHsx :: QuasiQuoter
+myTagsOnlyHsx = customHsx
+ (HsxSettings { checkMarkup = True
+ , additionalTagNames = Set.fromList ["mycustomtag", "anothercustomtag"]
+ , additionalAttributeNames = Set.fromList []
+ }
+ )
+
+myAttrsOnlyHsx :: QuasiQuoter
+myAttrsOnlyHsx = customHsx
+ (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 11b6c1506..8087828a5 100644
--- a/ihp-hsx/Test/IHP/HSX/ParserSpec.hs
+++ b/ihp-hsx/Test/IHP/HSX/ParserSpec.hs
@@ -11,68 +11,162 @@ 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 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 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 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 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 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 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 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 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 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 position extensions " "
+ let p = parseHsx settings 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 settings 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 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 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 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 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 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 settings = HsxSettings False Set.empty Set.empty
+ it "should not check markup" do
+ 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 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 settings position extensions "
"
+ (Megaparsec.errorBundlePretty error) `shouldBe` errorText
+
+ it "should parse a closing tag with spaces" do
+ let p = parseHsx settings position extensions ""
+ p `shouldBe` (Right (Children [Node "div" [] [] False]))
+
+ it "should strip spaces around nodes" do
+ 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 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 settings position extensions " Hello World "
+ p `shouldBe` (Right (Children [TextNode "Hello World"]))
+
+ it "should deal with variables in text nodes" do
+ 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 settings position extensions " "
+ p `shouldBe` (Right (Children [Node "div" [] [] False]))
+
+ it "should collapse spaces" do
+ 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 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 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 settings position extensions "
"
+ p `shouldBe` (Right (Children [Node "div" [StaticAttribute "data-client_id" (TextValue "test")] [] False]))
+
+ it "should accept doctype" do
+ 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
diff --git a/ihp-hsx/ihp-hsx.cabal b/ihp-hsx/ihp-hsx.cabal
index b6973912d..8ee565b4b 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