ihp-hsx (empty) → 0.18.0
raw patch · 8 files changed
+1361/−0 lines, 8 filesdep +basedep +blaze-htmldep +blaze-markup
Dependencies added: base, blaze-html, blaze-markup, bytestring, containers, haskell-src-meta, megaparsec, string-conversions, template-haskell, text
Files
- IHP/HSX/ConvertibleStrings.hs +39/−0
- IHP/HSX/Parser.hs +632/−0
- IHP/HSX/QQ.hs +132/−0
- IHP/HSX/ToHtml.hs +44/−0
- LICENSE +21/−0
- README.md +406/−0
- changelog.md +5/−0
- ihp-hsx.cabal +82/−0
+ IHP/HSX/ConvertibleStrings.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}++module IHP.HSX.ConvertibleStrings where++import Prelude+import Data.String.Conversions (ConvertibleStrings (convertString), cs)+import Text.Blaze.Html5+import Data.Text+import Data.ByteString+import qualified Text.Blaze.Html5 as Html5+import qualified Data.ByteString.Lazy as LBS++instance ConvertibleStrings String Html5.AttributeValue where+ {-# INLINE convertString #-}+ convertString = stringValue++instance ConvertibleStrings Text Html5.AttributeValue where+ {-# INLINE convertString #-}+ convertString = Html5.textValue++instance ConvertibleStrings String Html5.Html where+ {-# INLINE convertString #-}+ convertString = Html5.string++instance ConvertibleStrings ByteString Html5.AttributeValue where+ {-# INLINE convertString #-}+ convertString value = convertString (cs value :: Text)++instance ConvertibleStrings LBS.ByteString Html5.AttributeValue where+ {-# INLINE convertString #-}+ convertString value = convertString (cs value :: Text)++instance ConvertibleStrings Text Html5.Html where+ {-# INLINE convertString #-}+ convertString = Html5.text
+ IHP/HSX/Parser.hs view
@@ -0,0 +1,632 @@+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeFamilies #-}+{-|+Module: IHP.HSX.Parser+Description: Parser for HSX code+Copyright: (c) digitally induced GmbH, 2022+-}+module IHP.HSX.Parser+( parseHsx+, Node (..)+, Attribute (..)+, AttributeValue (..)+, collapseSpace+) where++import Prelude+import Data.Text+import Data.Set+import Text.Megaparsec+import Text.Megaparsec.Char+import Data.Void+import qualified Data.Char as Char+import qualified Data.Text as Text+import Control.Monad.Fail+import Data.String.Conversions+import qualified Data.List as List+import Control.Monad (unless)+import Prelude (show)+import qualified Language.Haskell.Meta as Haskell+import qualified Language.Haskell.TH.Syntax as Haskell+import qualified "template-haskell" Language.Haskell.TH as TH+import qualified Data.Set as Set+import qualified Data.Containers.ListUtils as List++data AttributeValue = TextValue !Text | ExpressionValue !Haskell.Exp deriving (Eq, Show)++data Attribute = StaticAttribute !Text !AttributeValue | SpreadAttributes !Haskell.Exp deriving (Eq, Show)++data Node = Node !Text ![Attribute] ![Node] !Bool+ | TextNode !Text+ | PreEscapedTextNode !Text -- ^ Used in @script@ or @style@ bodies+ | SplicedNode !Haskell.Exp -- ^ Inline haskell expressions like @{myVar}@ or @{f "hello"}@+ | Children ![Node]+ | CommentNode !Text+ deriving (Eq, Show)++-- | Parses a HSX text and returns a 'Node'+--+-- __Example:__+--+-- > let filePath = "my-template"+-- > let line = 0+-- > let col = 0+-- > let position = Megaparsec.SourcePos filePath (Megaparsec.mkPos line) (Megaparsec.mkPos col)+-- > let hsxText = "<strong>Hello</strong>"+-- >+-- > let (Right node) = parseHsx position hsxText+parseHsx :: SourcePos -> Text -> Either (ParseErrorBundle Text Void) Node+parseHsx position code = runParser (setPosition position *> parser) "" code++type Parser = Parsec Void Text++setPosition pstateSourcePos = updateParserState (\state -> state {+ statePosState = (statePosState state) { pstateSourcePos }+ })++parser :: Parser Node+parser = do+ space+ node <- manyHsxElement <|> hsxElement+ space+ eof+ pure node++hsxElement = try hsxComment <|> try hsxSelfClosingElement <|> hsxNormalElement++manyHsxElement = do+ children <- many hsxChild+ pure (Children (stripTextNodeWhitespaces children))++hsxSelfClosingElement = do+ _ <- char '<'+ name <- hsxElementName+ let isLeaf = name `Set.member` leafs+ attributes <-+ if isLeaf+ then hsxNodeAttributes (string ">" <|> string "/>")+ else hsxNodeAttributes (string "/>")+ space+ pure (Node name attributes [] isLeaf)++hsxNormalElement = do+ (name, attributes) <- hsxOpeningElement+ let parsePreEscapedTextChildren transformText = do+ let closingElement = "</" <> name <> ">"+ text <- cs <$> manyTill anySingle (string closingElement)+ pure [PreEscapedTextNode (transformText text)]+ let parseNormalHSXChildren = stripTextNodeWhitespaces <$> (space >> (manyTill (try hsxChild) (try (space >> hsxClosingElement name))))++ -- script and style tags have special handling for their children. Inside those tags+ -- we allow any kind of content. Using a haskell expression like @<script>{myHaskellExpr}</script>@+ -- will just literally output the string @{myHaskellExpr}@ without evaluating the haskell expression itself.+ --+ -- Here is an example HSX code explaining the problem:+ -- [hsx|<style>h1 { color: red; }</style>|]+ -- The @{ color: red; }@ would be parsed as a inline haskell expression without the special handling+ --+ -- Additionally we don't do the usual escaping for style and script bodies, as this will make e.g. the+ -- javascript unusuable.+ children <- case name of+ "script" -> parsePreEscapedTextChildren Text.strip+ "style" -> parsePreEscapedTextChildren (collapseSpace . Text.strip)+ otherwise -> parseNormalHSXChildren+ pure (Node name attributes children False)++hsxOpeningElement = do+ char '<'+ name <- hsxElementName+ space+ attributes <- hsxNodeAttributes (char '>')+ pure (name, attributes)++hsxComment :: Parser Node+hsxComment = do+ string "<!--"+ body :: String <- manyTill (satisfy (const True)) (string "-->")+ space+ pure (CommentNode (cs body))+++hsxNodeAttributes :: Parser a -> Parser [Attribute]+hsxNodeAttributes end = staticAttributes+ where+ staticAttributes = do+ attributes <- manyTill (hsxNodeAttribute <|> hsxSplicedAttributes) end+ let staticAttributes = List.filter isStaticAttribute attributes+ let keys = List.map (\(StaticAttribute name _) -> name) staticAttributes+ let uniqueKeys = List.nubOrd keys+ unless (keys == uniqueKeys) (fail $ "Duplicate attribute found in tag: " <> show (keys List.\\ uniqueKeys))+ pure attributes++isStaticAttribute (StaticAttribute _ _) = True+isStaticAttribute _ = False++hsxSplicedAttributes :: Parser Attribute+hsxSplicedAttributes = do+ name <- between (string "{...") (string "}") (takeWhile1P Nothing (\c -> c /= '}'))+ space+ haskellExpression <- case Haskell.parseExp (cs name) of+ Right expression -> pure (patchExpr expression)+ Left error -> fail (show error)+ pure (SpreadAttributes haskellExpression)++hsxNodeAttribute = do+ key <- hsxAttributeName+ space++ -- Boolean attributes like <input disabled/> will be represented as <input disabled="disabled"/>+ -- as there is currently no other way to represent them with blaze-html.+ --+ -- There's a special case for data attributes: Data attributes like <form data-disable-javascript-submission/> will be represented as <form data-disable-javascript-submission="true"/>+ --+ -- See: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes+ let attributeWithoutValue = do+ let value = if "data-" `Text.isPrefixOf` key+ then "true"+ else key+ pure (StaticAttribute key (TextValue value))++ -- Parsing normal attributes like <input value="Hello"/>+ let attributeWithValue = do+ _ <- char '='+ space+ value <- hsxQuotedValue <|> hsxSplicedValue+ space+ pure (StaticAttribute key value)++ attributeWithValue <|> attributeWithoutValue+++hsxAttributeName :: Parser Text+hsxAttributeName = do+ name <- rawAttribute+ unless (isValidAttributeName name) (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 == '-')+++hsxQuotedValue :: Parser AttributeValue+hsxQuotedValue = do+ value <- between (char '"') (char '"') (takeWhileP Nothing (\c -> c /= '\"'))+ pure (TextValue value)++hsxSplicedValue :: Parser AttributeValue+hsxSplicedValue = do+ value <- between (char '{') (char '}') (takeWhile1P Nothing (\c -> c /= '}'))+ haskellExpression <- case Haskell.parseExp (cs value) of+ Right expression -> pure (patchExpr expression)+ Left error -> fail (show error)+ pure (ExpressionValue haskellExpression)++hsxClosingElement name = (hsxClosingElement' name) <?> friendlyErrorMessage+ where+ friendlyErrorMessage = show (Text.unpack ("</" <> name <> ">"))+ hsxClosingElement' name = do+ _ <- string ("</" <> name)+ space+ char ('>')+ pure ()++hsxChild = hsxElement <|> hsxSplicedNode <|> try (space >> hsxElement) <|> hsxText++-- | Parses a hsx text node+--+-- Stops parsing when hitting a variable, like `{myVar}`+hsxText :: Parser Node+hsxText = buildTextNode <$> takeWhile1P (Just "text") (\c -> c /= '{' && c /= '}' && c /= '<' && c /= '>')++-- | Builds a TextNode and strips all surround whitespace from the input string+buildTextNode :: Text -> Node+buildTextNode value = TextNode (collapseSpace value)++data TokenTree = TokenLeaf Text | TokenNode [TokenTree] deriving (Show)++hsxSplicedNode :: Parser Node+hsxSplicedNode = do+ expression <- doParse+ haskellExpression <- case Haskell.parseExp (cs expression) of+ Right expression -> pure (patchExpr expression)+ Left error -> fail (show error)+ pure (SplicedNode haskellExpression)+ where+ doParse = do+ tree <- node+ let value = (treeToString "" tree)+ pure $ Text.init $ Text.tail value++ parseTree = node <|> leaf+ node = TokenNode <$> between (char '{') (char '}') (many parseTree)+ leaf = TokenLeaf <$> takeWhile1P Nothing (\c -> c /= '{' && c /= '}')+ treeToString :: Text -> TokenTree -> Text+ treeToString acc (TokenLeaf value) = acc <> value+ treeToString acc (TokenNode []) = acc+ 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 == '-')+ 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)+ space+ pure name++hsxIdentifier :: Parser Text+hsxIdentifier = do+ name <- takeWhile1P (Just "identifier") (\c -> Char.isAlphaNum c || c == '_')+ space+ pure name+++attributes :: Set Text+attributes = Set.fromList+ [ "accept", "accept-charset", "accesskey", "action", "alt", "async"+ , "autocomplete", "autofocus", "autoplay", "challenge", "charset"+ , "checked", "cite", "class", "cols", "colspan", "content"+ , "contenteditable", "contextmenu", "controls", "coords", "data"+ , "datetime", "defer", "dir", "disabled", "draggable", "enctype"+ , "form", "formaction", "formenctype", "formmethod", "formnovalidate"+ , "for"+ , "formtarget", "headers", "height", "hidden", "high", "href"+ , "hreflang", "http-equiv", "icon", "id", "ismap", "item", "itemprop"+ , "itemscope", "itemtype"+ , "keytype", "label", "lang", "list", "loop", "low", "manifest", "max"+ , "maxlength", "media", "method", "min", "multiple", "name"+ , "novalidate", "onbeforeonload", "onbeforeprint", "onblur", "oncanplay"+ , "oncanplaythrough", "onchange", "oncontextmenu", "onclick"+ , "ondblclick", "ondrag", "ondragend", "ondragenter", "ondragleave"+ , "ondragover", "ondragstart", "ondrop", "ondurationchange", "onemptied"+ , "onended", "onerror", "onfocus", "onformchange", "onforminput"+ , "onhaschange", "oninput", "oninvalid", "onkeydown", "onkeyup"+ , "onload", "onloadeddata", "onloadedmetadata", "onloadstart"+ , "onmessage", "onmousedown", "onmousemove", "onmouseout", "onmouseover"+ , "onmouseup", "onmousewheel", "ononline", "onpagehide", "onpageshow"+ , "onpause", "onplay", "onplaying", "onprogress", "onpropstate"+ , "onratechange", "onreadystatechange", "onredo", "onresize", "onscroll"+ , "onseeked", "onseeking", "onselect", "onstalled", "onstorage"+ , "onsubmit", "onsuspend", "ontimeupdate", "onundo", "onunload"+ , "onvolumechange", "onwaiting", "open", "optimum", "pattern", "ping"+ , "placeholder", "preload", "pubdate", "radiogroup", "readonly", "rel"+ , "required", "reversed", "rows", "rowspan", "sandbox", "scope"+ , "scoped", "seamless", "selected", "shape", "size", "sizes", "span"+ , "spellcheck", "src", "srcdoc", "srcset", "start", "step", "style", "subject"+ , "summary", "tabindex", "target", "title", "type", "usemap", "value"+ , "width", "wrap", "xmlns"+ , "ontouchstart", "download"+ , "allowtransparency", "minlength", "maxlength", "property"+ , "role"+ , "d", "viewBox", "cx", "cy", "r", "x", "y", "text-anchor", "alignment-baseline"+ , "line-spacing", "letter-spacing"+ , "integrity", "crossorigin", "poster"+ , "accent-height", "accumulate", "additive", "alphabetic", "amplitude"+ , "arabic-form", "ascent", "attributeName", "attributeType", "azimuth"+ , "baseFrequency", "baseProfile", "bbox", "begin", "bias", "by", "calcMode"+ , "cap-height", "class", "clipPathUnits", "contentScriptType"+ , "contentStyleType", "cx", "cy", "d", "descent", "diffuseConstant", "divisor"+ , "dur", "dx", "dy", "edgeMode", "elevation", "end", "exponent"+ , "externalResourcesRequired", "filterRes", "filterUnits", "font-family"+ , "font-size", "font-stretch", "font-style", "font-variant", "font-weight"+ , "format", "from", "fx", "fy", "g1", "g2", "glyph-name", "glyphRef"+ , "gradientTransform", "gradientUnits", "hanging", "height", "horiz-adv-x"+ , "horiz-origin-x", "horiz-origin-y", "id", "ideographic", "in", "in2"+ , "intercept", "k", "k1", "k2", "k3", "k4", "kernelMatrix", "kernelUnitLength"+ , "keyPoints", "keySplines", "keyTimes", "lang", "lengthAdjust"+ , "limitingConeAngle", "local", "markerHeight", "markerUnits", "markerWidth"+ , "maskContentUnits", "maskUnits", "mathematical", "max", "media", "method"+ , "min", "mode", "name", "numOctaves", "offset", "onabort", "onactivate"+ , "onbegin", "onclick", "onend", "onerror", "onfocusin", "onfocusout", "onload"+ , "onmousedown", "onmousemove", "onmouseout", "onmouseover", "onmouseup"+ , "onrepeat", "onresize", "onscroll", "onunload", "onzoom", "operator", "order"+ , "orient", "orientation", "origin", "overline-position", "overline-thickness"+ , "panose-1", "path", "pathLength", "patternContentUnits", "patternTransform"+ , "patternUnits", "points", "pointsAtX", "pointsAtY", "pointsAtZ"+ , "preserveAlpha", "preserveAspectRatio", "primitiveUnits", "r", "radius"+ , "refX", "refY", "rendering-intent", "repeatCount", "repeatDur"+ , "requiredExtensions", "requiredFeatures", "restart", "result", "rotate", "rx"+ , "ry", "scale", "seed", "slope", "spacing", "specularConstant"+ , "specularExponent", "spreadMethod", "startOffset", "stdDeviation", "stemh"+ , "stemv", "stitchTiles", "strikethrough-position", "strikethrough-thickness"+ , "string", "style", "surfaceScale", "systemLanguage", "tableValues", "target"+ , "targetX", "targetY", "textLength", "title", "to", "transform", "type", "u1"+ , "u2", "underline-position", "underline-thickness", "unicode", "unicode-range"+ , "units-per-em", "v-alphabetic", "v-hanging", "v-ideographic", "v-mathematical"+ , "values", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "viewBox"+ , "viewTarget", "width", "widths", "x", "x-height", "x1", "x2"+ , "xChannelSelector", "xlink:actuate", "xlink:arcrole", "xlink:href"+ , "xlink:role", "xlink:show", "xlink:title", "xlink:type", "xml:base"+ , "xml:lang", "xml:space", "y", "y1", "y2", "yChannelSelector", "z", "zoomAndPan"+ , "alignment-baseline", "baseline-shift", "clip-path", "clip-rule"+ , "clip", "color-interpolation-filters", "color-interpolation"+ , "color-profile", "color-rendering", "color", "cursor", "direction"+ , "display", "dominant-baseline", "enable-background", "fill-opacity"+ , "fill-rule", "fill", "filter", "flood-color", "flood-opacity"+ , "font-size-adjust", "glyph-orientation-horizontal"+ , "glyph-orientation-vertical", "image-rendering", "kerning", "letter-spacing"+ , "lighting-color", "marker-end", "marker-mid", "marker-start", "mask"+ , "opacity", "overflow", "pointer-events", "shape-rendering", "stop-color"+ , "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap"+ , "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width"+ , "stroke", "text-anchor", "text-decoration", "text-rendering", "unicode-bidi"+ , "visibility", "word-spacing", "writing-mode", "is"+ , "cellspacing", "cellpadding", "bgcolor", "classes"+ , "loading"+ , "frameborder", "allow", "allowfullscreen", "nonce", "referrerpolicy"+ ]++parents :: Set Text+parents = Set.fromList+ [ "a"+ , "abbr"+ , "address"+ , "animate"+ , "animateMotion"+ , "animateTransform"+ , "article"+ , "aside"+ , "audio"+ , "b"+ , "bdi"+ , "bdo"+ , "blink"+ , "blockquote"+ , "body"+ , "button"+ , "canvas"+ , "caption"+ , "circle"+ , "cite"+ , "clipPath"+ , "code"+ , "colgroup"+ , "command"+ , "data"+ , "datalist"+ , "dd"+ , "defs"+ , "del"+ , "desc"+ , "details"+ , "dfn"+ , "dialog"+ , "discard"+ , "div"+ , "dl"+ , "dt"+ , "ellipse"+ , "em"+ , "feBlend"+ , "feColorMatrix"+ , "feComponentTransfer"+ , "feComposite"+ , "feConvolveMatrix"+ , "feDiffuseLighting"+ , "feDisplacementMap"+ , "feDistantLight"+ , "feDropShadow"+ , "feFlood"+ , "feFuncA"+ , "feFuncB"+ , "feFuncG"+ , "feFuncR"+ , "feGaussianBlur"+ , "feImage"+ , "feMerge"+ , "feMergeNode"+ , "feMorphology"+ , "feOffset"+ , "fePointLight"+ , "feSpecularLighting"+ , "feSpotLight"+ , "feTile"+ , "feTurbulence"+ , "fieldset"+ , "figcaption"+ , "figure"+ , "filter"+ , "footer"+ , "foreignObject"+ , "form"+ , "g"+ , "h1"+ , "h2"+ , "h3"+ , "h4"+ , "h5"+ , "h6"+ , "hatch"+ , "hatchpath"+ , "head"+ , "header"+ , "hgroup"+ , "html"+ , "i"+ , "iframe"+ , "ins"+ , "ion-icon"+ , "kbd"+ , "label"+ , "legend"+ , "li"+ , "line"+ , "linearGradient"+ , "loading"+ , "main"+ , "map"+ , "mark"+ , "marker"+ , "marquee"+ , "mask"+ , "menu"+ , "mesh"+ , "meshgradient"+ , "meshpatch"+ , "meshrow"+ , "metadata"+ , "meter"+ , "mpath"+ , "nav"+ , "noscript"+ , "object"+ , "ol"+ , "optgroup"+ , "option"+ , "output"+ , "p"+ , "path"+ , "pattern"+ , "picture"+ , "polygon"+ , "polyline"+ , "pre"+ , "progress"+ , "q"+ , "radialGradient"+ , "rect"+ , "rp"+ , "rt"+ , "ruby"+ , "s"+ , "samp"+ , "script"+ , "section"+ , "select"+ , "set"+ , "slot"+ , "small"+ , "source"+ , "span"+ , "stop"+ , "strong"+ , "style"+ , "sub"+ , "summary"+ , "sup"+ , "svg"+ , "switch"+ , "symbol"+ , "table"+ , "tbody"+ , "td"+ , "template"+ , "text"+ , "textPath"+ , "textarea"+ , "tfoot"+ , "th"+ , "thead"+ , "time"+ , "title"+ , "tr"+ , "track"+ , "tspan"+ , "u"+ , "ul"+ , "unknown"+ , "use"+ , "var"+ , "video"+ , "view"+ , "wbr"+ ]++leafs :: Set Text+leafs = Set.fromList+ [ "area"+ , "base"+ , "br"+ , "col"+ , "embed"+ , "hr"+ , "img"+ , "input"+ , "link"+ , "meta"+ , "param"+ ]++stripTextNodeWhitespaces nodes = stripLastTextNodeWhitespaces (stripFirstTextNodeWhitespaces nodes)++stripLastTextNodeWhitespaces nodes =+ let strippedLastElement = if List.length nodes > 0+ then case List.last nodes of+ TextNode text -> Just $ TextNode (Text.stripEnd text)+ otherwise -> Nothing+ else Nothing+ in case strippedLastElement of+ Just last -> (fst $ List.splitAt ((List.length nodes) - 1) nodes) <> [last]+ Nothing -> nodes++stripFirstTextNodeWhitespaces nodes =+ let strippedFirstElement = if List.length nodes > 0+ then case List.head nodes of+ TextNode text -> Just $ TextNode (Text.stripStart text)+ otherwise -> Nothing+ else Nothing+ in case strippedFirstElement of+ Just first -> first:(List.tail nodes)+ Nothing -> nodes++-- | Replaces multiple space characters with a single one+collapseSpace :: Text -> Text+collapseSpace text = cs $ filterDuplicateSpaces (cs text)+ where+ filterDuplicateSpaces :: String -> String+ filterDuplicateSpaces string = filterDuplicateSpaces' string False++ filterDuplicateSpaces' :: String -> Bool -> String+ 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 = []+++patchExpr :: TH.Exp -> TH.Exp+patchExpr (TH.UInfixE (TH.VarE varName) (TH.VarE hash) (TH.VarE labelValue)) | hash == TH.mkName "#" = TH.AppE (TH.VarE varName) fromLabel+ where+ fromLabel = TH.AppTypeE (TH.VarE (TH.mkName "fromLabel")) (TH.LitT (TH.StrTyLit (show labelValue)))+--- UInfixE (UInfixE a (VarE |>) (VarE get)) (VarE #) (VarE firstName)+patchExpr input@(TH.UInfixE (TH.UInfixE a (TH.VarE arrow) (TH.VarE get)) (TH.VarE hash) (TH.VarE labelValue)) | (hash == TH.mkName "#") && (arrow == TH.mkName "|>") && (get == TH.mkName "get") =+ (TH.UInfixE (patchExpr a) (TH.VarE arrow) (TH.AppE (TH.VarE get) fromLabel))+ where+ fromLabel = TH.AppTypeE (TH.VarE (TH.mkName "fromLabel")) (TH.LitT (TH.StrTyLit (show labelValue)))+-- UInfixE (UInfixE a (VarE $) (VarE get)) (VarE #) (AppE (VarE id) (VarE checklist))+patchExpr (TH.UInfixE (TH.UInfixE a b get) (TH.VarE hash) (TH.AppE (TH.VarE labelValue) (TH.VarE d))) | (hash == TH.mkName "#") =+ TH.UInfixE (patchExpr a) (patchExpr b) (TH.AppE (TH.AppE get fromLabel) (TH.VarE d))+ where+ fromLabel = TH.AppTypeE (TH.VarE (TH.mkName "fromLabel")) (TH.LitT (TH.StrTyLit (show labelValue)))+patchExpr (TH.UInfixE (TH.VarE varName) (TH.VarE hash) (TH.AppE (TH.VarE labelValue) arg)) | hash == TH.mkName "#" = TH.AppE (TH.AppE (TH.VarE varName) fromLabel) arg+ where+ fromLabel = TH.AppTypeE (TH.VarE (TH.mkName "fromLabel")) (TH.LitT (TH.StrTyLit (show labelValue)))+patchExpr (TH.UInfixE (TH.VarE a) (TH.VarE hash) (TH.AppE (TH.VarE labelValue) (TH.VarE b))) | hash == TH.mkName "#" =+ TH.AppE (TH.AppE (TH.VarE a) fromLabel) (TH.VarE b)+ where+ fromLabel = TH.AppTypeE (TH.VarE (TH.mkName "fromLabel")) (TH.LitT (TH.StrTyLit (show labelValue)))++patchExpr (TH.UInfixE a b c) = TH.UInfixE (patchExpr a) (patchExpr b) (patchExpr c)+patchExpr (TH.ParensE e) = TH.ParensE (patchExpr e)+patchExpr (TH.RecUpdE a b) = TH.RecUpdE (patchExpr a) b+patchExpr (TH.AppE a b) = TH.AppE (patchExpr a) (patchExpr b)+patchExpr (TH.LamE a b) = TH.LamE a (patchExpr b)+patchExpr (TH.LetE a b) = TH.LetE a' (patchExpr b)+ where+ a' = List.map patchDec a+ patchDec (TH.ValD a (TH.NormalB b) c) = (TH.ValD a (TH.NormalB (patchExpr b)) c)+ patchDec a = a+patchExpr (TH.CondE a b c) = TH.CondE (patchExpr a) (patchExpr b) (patchExpr c)+patchExpr (TH.SigE a b) = TH.SigE (patchExpr a) b+patchExpr e = e
+ IHP/HSX/QQ.hs view
@@ -0,0 +1,132 @@+{-# LANGUAGE TemplateHaskell, UndecidableInstances, BangPatterns, PackageImports, FlexibleInstances, OverloadedStrings #-}++{-|+Module: IHP.HSX.QQ+Description: Defines the @[hsx||]@ syntax+Copyright: (c) digitally induced GmbH, 2022+-}+module IHP.HSX.QQ (hsx) where++import Prelude+import qualified Data.Text as Text+import Data.Text (Text)+import IHP.HSX.Parser+import qualified "template-haskell" Language.Haskell.TH as TH+import qualified "template-haskell" Language.Haskell.TH.Syntax as TH+import Language.Haskell.TH.Quote+import Text.Blaze.Html5 ((!))+import qualified Text.Blaze.Html5 as Html5+import Text.Blaze.Html (Html)+import Text.Blaze.Internal (attribute, MarkupM (Parent, Leaf), StaticString (..))+import Data.String.Conversions+import IHP.HSX.ToHtml+import Control.Monad.Fail+import qualified Text.Megaparsec as Megaparsec+import qualified Text.Blaze.Html.Renderer.String as BlazeString+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text+import Data.List (foldl')++hsx :: QuasiQuoter+hsx = QuasiQuoter {+ quoteExp = quoteHsxExpression,+ quotePat = error "quotePat: not defined",+ quoteDec = error "quoteDec: not defined",+ quoteType = error "quoteType: not defined"+ }++quoteHsxExpression :: String -> TH.ExpQ+quoteHsxExpression code = do+ hsxPosition <- findHSXPosition+ expression <- case parseHsx hsxPosition (cs code) of+ Left error -> fail (Megaparsec.errorBundlePretty error)+ Right result -> pure result+ compileToHaskell expression+ where++ findHSXPosition = do+ loc <- TH.location+ let (line, col) = TH.loc_start loc+ pure $ Megaparsec.SourcePos (TH.loc_filename loc) (Megaparsec.mkPos line) (Megaparsec.mkPos col)++compileToHaskell :: Node -> TH.ExpQ+compileToHaskell (Node name attributes children isLeaf) =+ let+ renderedChildren = TH.listE $ map compileToHaskell children+ stringAttributes = TH.listE $ map toStringAttribute attributes+ openTag :: Text+ openTag = "<" <> tag+ tag :: Text+ tag = cs name+ in+ if isLeaf+ then+ let+ closeTag :: Text+ closeTag = ">"+ in [| (applyAttributes (Leaf (textToStaticString $(TH.lift tag)) (textToStaticString $(TH.lift openTag)) (textToStaticString $(TH.lift closeTag)) ()) $(stringAttributes)) |]+ else+ let+ closeTag :: Text+ closeTag = "</" <> tag <> ">"+ in [| (applyAttributes (makeParent (textToStaticString $(TH.lift name)) (textToStaticString $(TH.lift openTag)) (textToStaticString $(TH.lift closeTag)) $renderedChildren) $(stringAttributes)) |]++compileToHaskell (Children children) =+ let+ renderedChildren = TH.listE $ map compileToHaskell children+ in [| mconcat $(renderedChildren) |]++compileToHaskell (TextNode value) = [| Html5.preEscapedText value |]+compileToHaskell (PreEscapedTextNode value) = [| Html5.preEscapedText value |]+compileToHaskell (SplicedNode expression) = [| toHtml $(pure expression) |]+compileToHaskell (CommentNode value) = [| Html5.textComment value |]+++toStringAttribute :: Attribute -> TH.ExpQ+toStringAttribute (StaticAttribute name (TextValue value)) = do+ let nameWithSuffix = " " <> name <> "=\""+ if Text.null value+ then [| \h -> h ! ((attribute (Html5.textTag name) (Html5.textTag nameWithSuffix)) mempty) |]+ else [| \h -> h ! ((attribute (Html5.textTag name) (Html5.textTag nameWithSuffix)) (Html5.preEscapedTextValue value)) |]++toStringAttribute (StaticAttribute name (ExpressionValue expression)) = let nameWithSuffix = " " <> name <> "=\"" in [| applyAttribute name nameWithSuffix $(pure expression) |]+toStringAttribute (SpreadAttributes expression) = [| spreadAttributes $(pure expression) |]++spreadAttributes :: ApplyAttribute value => [(Text, value)] -> Html5.Html -> Html5.Html+spreadAttributes attributes html = applyAttributes html $ map (\(name, value) -> applyAttribute name (" " <> name <> "=\"") value) attributes++applyAttributes :: Html5.Html -> [Html5.Html -> Html5.Html] -> Html5.Html+applyAttributes element attributes = foldl' (\element attribute -> attribute element) element attributes+{-# INLINE applyAttributes #-}++makeParent :: StaticString -> StaticString -> StaticString -> [Html] -> Html+makeParent tag openTag closeTag children = Parent tag openTag closeTag (mconcat children)+{-# INLINE makeParent #-}++textToStaticString :: Text -> StaticString+textToStaticString text = StaticString (Text.unpack text ++) (Text.encodeUtf8 text) text+{-# INLINE textToStaticString #-}++class ApplyAttribute value where+ applyAttribute :: Text -> Text -> value -> (Html5.Html -> Html5.Html)++instance ApplyAttribute Bool where+ applyAttribute attr attr' True h = h ! (attribute (Html5.textTag attr) (Html5.textTag attr') (Html5.textValue value))+ where+ value = if "data-" `Text.isPrefixOf` attr+ then "true" -- "true" for data attributes+ else attr -- normal html boolean attriubtes, like <input disabled="disabled"/>, see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes+ applyAttribute attr attr' false h | "data-" `Text.isPrefixOf` attr = h ! (attribute (Html5.textTag attr) (Html5.textTag attr') "false") -- data attribute set to "false"+ applyAttribute attr attr' false h = h -- html boolean attribute, like <input disabled/> will be dropped as there is no other way to specify that it's set to false+ {-# INLINE applyAttribute #-}++instance ApplyAttribute attribute => ApplyAttribute (Maybe attribute) where+ applyAttribute attr attr' (Just value) h = applyAttribute attr attr' value h+ applyAttribute attr attr' Nothing h = h++instance {-# OVERLAPPABLE #-} ConvertibleStrings value Html5.AttributeValue => ApplyAttribute value where+ applyAttribute attr attr' value h = h ! (attribute (Html5.textTag attr) (Html5.textTag attr') (cs value))+ {-# INLINE applyAttribute #-}++instance Show (MarkupM ()) where+ show html = BlazeString.renderHtml html
+ IHP/HSX/ToHtml.hs view
@@ -0,0 +1,44 @@+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE FlexibleInstances #-}++{-|+Module: IHP.HSX.ToHtml+Description: Provides a few helper instances that convert data structures to HTML+Copyright: (c) digitally induced GmbH, 2022+-}+module IHP.HSX.ToHtml where++import Prelude+import qualified Text.Blaze.Html5 as Html5+import qualified Text.Blaze.Internal+import Data.Text+import Data.ByteString+import Data.String.Conversions (cs)+import IHP.HSX.ConvertibleStrings ()++class ToHtml a where+ toHtml :: a -> Html5.Html++instance ToHtml (Text.Blaze.Internal.MarkupM ()) where+ {-# INLINE toHtml #-}+ toHtml a = a++instance ToHtml Text where+ {-# INLINE toHtml #-}+ toHtml = Html5.text++instance ToHtml String where+ {-# INLINE toHtml #-}+ toHtml = Html5.string++instance ToHtml ByteString where+ {-# INLINE toHtml #-}+ toHtml value = toHtml (cs value :: Text)++instance {-# OVERLAPPABLE #-} ToHtml a => ToHtml (Maybe a) where+ {-# INLINE toHtml #-}+ toHtml maybeValue = maybe mempty toHtml maybeValue++instance {-# OVERLAPPABLE #-} Show a => ToHtml a where+ {-# INLINE toHtml #-}+ toHtml value = Html5.string (show value)
+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License (MIT)++Copyright (c) 2020 digitally induced GmbH++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,406 @@+# HSX++This `ihp-hsx` package provides `[hsx|<h1>Hello World</h1>|]` syntax to IHP projects.++To use it outside of IHP, add this package to your cabal file and use the modules like this:++```haskell+import IHP.HSX.QQ -- <- The main thing+import IHP.HSX.ConvertibleStrings () -- <- Helper instancess+import IHP.HSX.ToHtml () -- <- More helper instances++import Text.Blaze.Html5 -- The blaze html library, IHP HSX is built on top of that++view :: Html+view = [hsx|+ <h1>Hello World</h1>+|]++-- To render the above view:+import Text.Blaze.Html.Renderer.Text (renderHtml)++rendered :: Text+rendered = renderHtml view+```++## Introduction++HSX can be written pretty much like normal HTML. You can write an HSX expression inside your Haskell code by wrapping it with [`[hsx|YOUR HSX CODE|]`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewPrelude.html#v:hsx). HSX expressions are just a syntax for [BlazeHtml](https://jaspervdj.be/blaze/) and thus are automatically escaped as described in the blaze documentation.++Because the HSX is parsed, you will get a syntax error when you type in invalid HTML.++### Inline Haskell++HSX can access Haskell variables wrapped with `{}` like this:++```haskell+let+ x :: Text = "World"+in+ [hsx|Hello {x}!|]+```++**If the variable is another HSX expression, a blaze HTML element, a text or string**: it is just included as you would expect.++**If the variable is any other custom Haskell data structure**: it will first be converted to a string representation by calling [`show`](https://ihp.digitallyinduced.com/api-docs/IHP-Prelude.html#v:show) on it. You can add a custom [`ToHtml`](https://ihp.digitallyinduced.com/api-docs/IHP-HSX-ToHtml.html#t:ToHtml) (import it from `IHP.HSX.ToHtml`) instance, to customize rendering a data structure.++You can also write more complex code like:++```haskell+let+ items :: [Int] = [ 0, 1, 2 ]+ renderItem n = [hsx|Hello {n}!|]+in+ [hsx|Complex demo: {forEach items renderItem}!|]+```++As the HSX expressions are compiled to Haskell code at compile-time, type errors inside these `{}` expressions will be reported to you by the compiler.++### Dynamic Attributes++The variable syntax can also be used in attribute values:++```haskell+let+ inputValue = "Hello World" :: Text+in+ [hsx|<input type="text" value={inputValue}/>|]+```++#### Boolean Attribute Values++HSX has special handling for Boolean values to make it easy to deal with HTML Boolean attributes like `disabled`, `readonly`, `checked`, etc.++You can write++```haskell+<input disabled={True} />+```++as a short form for:++```haskell+<input disabled="disabled" />+```++Writing `False`:++```haskell+<input disabled={False} />+```++This will not render the attribute, [as specified in the HTML standard](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes):++```haskell+<input />+```++#### Boolean data attributes++This behavior of omiting a attribute when it's set to `False` does not apply to `data-` attributes.++You can write++```haskell+<form data-disable-javascript-submission={True}/>+```++and it will render like this:++```html+<form data-disable-javascript-submission="true"/>+```++When set to `False`, like this:++```haskell+<form data-disable-javascript-submission={False}/>+```++the output HTML will keep the attribute and set it to `"false"`:++```html+<form data-disable-javascript-submission="false"/>+```++#### Maybe Attribute Values / Optional Attributes++HSX has special handling for Maybe values to make it easy to deal with optional attributes.++You can write++```haskell+let+ target :: Maybe Text+ target = Just "_blank"+in+ [hsx|<a target={target} />|]+```++and it will render to:++```haskell+<a target="_blank" />+```++Using `Nothing` results in the `target` attribute not being in the output HTML:++```haskell+let+ target :: Maybe Text+ target = Nothing+in+ [hsx|<a target={target} />|]+```++This will render to:++```haskell+<a />+```++### Spread Values++For dynamic use cases you can use `{...attributeList}`:++```haskell+<div { ...[ ("data-my-attribute" :: Text, "Hello World!" :: Text) ] } />+<div { ...[ ("data-user-" <> tshow userId, tshow userFirstname) ] } />+<div { ...someVariable } />+```++Note the `<>` concatenation operator.++### Special Elements: `<script>` and `<style>`++For `<script>` and `<style>` tags HSX applies some special handling. This only applies to tags with inline scripts or styles:++```html+<!-- No Special Handling: -->++<script src="/hello.js"></script>+<link rel="stylesheet" href="layout.css" />++<!-- Special Handling applies: -->++<script>+ alert("Hello");+</script>+<style>+ h1 {+ color: blue;+ }+</style>+```++Inside those tags using a Haskell expression will not work:++```haskell+<script>{myHaskellExpr}</script>+```++This will just literally output the string `{myHaskellExpr}` without evaluating the Haskell expression itself. This is because JavaScript usually uses `{}` for object expressions like `{ a: "hello" }`. The same applies to inline CSS inside `<style>` elements.++So using `{haskellVariables}` inside your JavaScript like this will not work:++```html+<script>+ var apiKey = "{apiKey}";+</script>+```++Instead use a `data-` attribute to solve this:++```html+<script data-api-key={apiKey}>+ var apiKey = document.currentScript.dataset.apiKey;+</script>+```++Additionally, HSX will not do the usual escaping for style and script bodies, as this will make e.g. the+JavaScript unusable.++### Syntax Rules++While most HTML is also valid HSX, there are some difference you need to be aware of:++#### Closing Tags++Tags always need to have a closing tag or be self-closing: so instead of `<br>` you need to write `<br/>`++#### JSX Differences++In JSX you have the restriction that you always have to have a single root tag. In HSX this restriction does not apply, so this is valid HSX (but not valid JSX):++```haskell+[hsx|+<div>A</div>+<div>B</div>+|]+```++#### Whitespace++Spaces and newline characters are removed where possible at HSX parse time.++#### Comments++HTML Comments are supported and can be used like this:++```html+<div>+ <!-- Begin of Main Section -->+ <h1>Hello</h1>+</div>+```++#### Empty Attributes++HSX allows you to write empty attributes like these:++```haskell+[hsx|+ <input disabled/>+|]+```++The underlying HTML library blaze currently does not support an empty HTML attribute. Therefore empty attributes are implemented by setting the attribute value to the attribute name. [This is valid HTML supported by all browsers.](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes). Therefore the generated HTML looks like this:++```html+<input disabled="disabled" />+```++#### Unescaped Strings++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`.+++## Common HSX Patterns++### Dealing with Maybe Values++When dealing with [`Maybe`](https://ihp.digitallyinduced.com/api-docs/IHP-Prelude.html#t:Maybe) values you sometimes want to conditionally render something. E.g. in react to render a tag with JSX you would do it like this:++```html+<p>Hi {user.name}</p>+{user.country && <p><small>from {user.country}</small></p>}+<p>Welcome!</p>+```++In HSX you usually write it like this:+++```haskell+render user = [hsx|+ <p>Hi {get #name user}</p>+ {renderCountry}+|]+ where+ renderCountry = case get #country user of+ Just country -> [hsx|<p><small>{country}</small></p>|]+ Nothing -> [hsx||]+```++What about if the country is a empty string? A simple solution could look like this:++```haskell+renderCountry = case get #country user of+ Just "" -> [hsx||]+ Just country -> [hsx|<p>from {country}!</p>|]+ Nothing -> [hsx||]+```++That code doesn't feel right. It's better to deal with the problem at create time of the `User` record already. Use [`emptyValueToNothing`](https://ihp.digitallyinduced.com/api-docs/IHP-Controller-Param.html#v:emptyValueToNothing) to transform an empty `Just ""` to `Nothing` before inserting it into the db:++```haskell+action CreateUserAction = do+ newRecord @User+ |> fill '["name", "country"]+ |> emptyValueToNothing #country+ |> createRecord+```++Now when the country input field is empty when creating the user, the `country` field will be set to `Nothing` instead of `Just ""`.++**The `emptyValueToNothing` function is part of the IHP framework and not bundled in this ihp-hsx package, so you might need to implement it yourself if you're using HSX outside of an IHP app.**++### Displaying HTML without Escaping++In HSX all variables and expressions are automatically escaped to avoid [XSS](https://en.wikipedia.org/wiki/Cross-site_scripting).++Let's say we have variable `myVariable = "<script>alert(1)</script>"`. This is how the HSX will behave:++```html+<div>{myVariable}</div>++-- RENDERS to this HTML:+<div>%3Cscript%3Ealert%281%29%3C/script%3E</div>+```++Sometimes you want to insert some actual HTML code to your HSX using a variable. E.g. when your app is rendering markdown, the generated HTML by the markdown library should not be escaped, the markdown library typically already takes care of that.++In these cases you can use [`preEscapedToHtml`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewPrelude.html#v:preEscapedToHtml) to mark a text as already escaped:++```html+<div>{markdownHtml |> preEscapedToHtml}</div>++-- Let's define `markdownHtml = "<p>hello</p>"`++-- This will render the following HTML:+<div><p>hello</p></div>+```++Be careful when using [`preEscapedToHtml`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewPrelude.html#v:preEscapedToHtml) as it can easily introduce security issues.++The [`preEscapedToHtml`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewPrelude.html#v:preEscapedToHtml) function can also be used to output HTML code that is not supported by HSX:+The [`preEscapedToHtml`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewPrelude.html#v:preEscapedToHtml) function can also be used to output HTML code that is not supported by HSX:++```html+{"<!--[if IE]> Internet Explorer Conditional Comments <![endif]-->" |> preEscapedToHtml}+```++## Example: HSX and the equivalent BlazeHtml++The following code using HSX:++```haskell+instance View EditView where+ html EditView { .. } = [hsx|+ <nav>+ <ol class="breadcrumb">+ <li class="breadcrumb-item"><a href={PostsAction}>Posts</a></li>+ <li class="breadcrumb-item active">Edit Post</li>+ </ol>+ </nav>+ <h1>Edit Post</h1>+ {renderForm post}+ |]+```++is roughly equivalent to the following BlazeHTML code:++```haskell+instance View EditView where+ html EditView { .. } = do+ H.nav $ do+ H.ol ! A.class_ "breadcrumb" $ do+ H.li ! A.class_ "breadcrumb-item" $ do+ H.a ! A.href (fromString (P.show PostsAction)) $ do+ "Posts"+ H.li ! A.class_ "breadcrumb-item active" $ do+ "Edit Post"+ H.h1 "Edit Post"+ renderForm post+```++given the following imports:++```haskell+module Web.View.Posts.Edit where+import Web.View.Prelude++import qualified Text.Blaze.Html5 as H+import qualified Text.Blaze.Html5.Attributes as A++import Prelude as P+```
+ changelog.md view
@@ -0,0 +1,5 @@+# Changelog for `ihp-hsx`++## Versions < 0.18:++[See the IHP main upgrade instructions](https://github.com/digitallyinduced/ihp/blob/master/UPGRADE.md)
+ ihp-hsx.cabal view
@@ -0,0 +1,82 @@+cabal-version: 2.2+name: ihp-hsx+version: 0.18.0+synopsis: JSX-like but for Haskell+description: JSX-like templating syntax for Haskell+license: MIT+license-file: LICENSE+author: digitally induced GmbH+maintainer: support@digitallyinduced.com+bug-reports: https://github.com/digitallyinduced/ihp/issues+category: HTML+build-type: Simple+extra-source-files: README.md, changelog.md++source-repository head+ type: git+ location: https://github.com/digitallyinduced/ihp.git++library+ default-language: Haskell2010+ build-depends:+ base >= 4.1 && <= 4.16+ , blaze-html >= 0.9.1 && < 0.10+ , bytestring >= 0.10.12 && < 0.11+ , text >= 1.2.4 && < 1.3+ , containers >= 0.6.5 && < 0.7+ , template-haskell >= 2.16.0 && < 2.17+ , blaze-markup >= 0.8.2 && < 0.9+ , haskell-src-meta >= 0.8.7 && < 0.9+ , megaparsec >= 9.0.1 && < 9.1+ , string-conversions >= 0.4.0 && < 0.5+ default-extensions:+ OverloadedStrings+ , NoImplicitPrelude+ , ImplicitParams+ , Rank2Types+ , NamedFieldPuns+ , TypeSynonymInstances+ , FlexibleInstances+ , DisambiguateRecordFields+ , DuplicateRecordFields+ , OverloadedLabels+ , FlexibleContexts+ , DataKinds+ , QuasiQuotes+ , TypeFamilies+ , PackageImports+ , ScopedTypeVariables+ , RecordWildCards+ , TypeApplications+ , DataKinds+ , InstanceSigs+ , DeriveGeneric+ , MultiParamTypeClasses+ , TypeOperators+ , DeriveDataTypeable+ , DefaultSignatures+ , BangPatterns+ , FunctionalDependencies+ , PartialTypeSignatures+ , BlockArguments+ , LambdaCase+ , StandaloneDeriving+ , TemplateHaskell+ ghc-options:+ -fstatic-argument-transformation+ -funbox-strict-fields+ -haddock+ -Wredundant-constraints+ -Wunused-imports+ -Wunused-foralls+ -Wmissing-fields+ -Winaccessible-code+ -Wmissed-specialisations+ -Wall-missed-specialisations+ -fexpose-all-unfoldings+ hs-source-dirs: .+ exposed-modules:+ IHP.HSX.Parser+ , IHP.HSX.QQ+ , IHP.HSX.ToHtml+ , IHP.HSX.ConvertibleStrings