Skip to content

Commit 92edec4

Browse files
committed
feat: be more like jsdom (when jsdom throws error if element name or attr name is invlaid - we ignore invalid chars) (NOTE: we could just use he npm package to escape all, no ignoring/cleaning)
1 parent 690f32b commit 92edec4

File tree

4 files changed

+201
-33
lines changed

4 files changed

+201
-33
lines changed

src/Halogen/VDom/DOM/StringRenderer.purs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Data.Number.Format as Data.Number.Format
1010
import Data.String as S
1111
import Data.Set as Set
1212
import Halogen.VDom.StringRenderer as VSR
13-
import Halogen.VDom.StringRenderer.Util (escape)
13+
import Halogen.VDom.StringRenderer.Util (escapeAttributeValue, escapeHtmlEntity)
1414
import Unsafe.Coerce (unsafeCoerce)
1515

1616
render i w. (w String) VDom (Array (Prop i)) w String
@@ -32,7 +32,7 @@ renderProp = case _ of
3232
Ref _ → Nothing
3333

3434
renderAttr String String Maybe String
35-
renderAttr name value = Just $ escape name <> "=\"" <> value <> "\""
35+
renderAttr name value = Just $ escapeHtmlEntity name <> "=\"" <> escapeAttributeValue value <> "\""
3636

3737
propNameToAttrName String String
3838
propNameToAttrName = case _ of
@@ -44,12 +44,11 @@ propNameToAttrName = case _ of
4444

4545
renderProperty String PropValue Maybe String
4646
renderProperty name prop = case typeOf (unsafeToForeign prop) of
47-
"string" → renderAttr name' $ (unsafeCoerce PropValue String) prop
48-
"number" → renderAttr name' $ Data.Number.Format.toString ((unsafeCoerce PropValue Number) prop)
47+
"string" → renderAttr name' $ (unsafeCoerce PropValue String) prop
48+
"number" → renderAttr name' $ Data.Number.Format.toString ((unsafeCoerce PropValue Number) prop)
4949
"boolean"
50-
if ((unsafeCoerce :: PropValue -> Boolean) prop)
51-
then Just $ escape name'
52-
else Nothing
50+
if ((unsafeCoerce :: PropValue -> Boolean) prop) then Just $ escapeHtmlEntity name'
51+
else Nothing
5352
_ → Nothing
5453
where
5554
name' = propNameToAttrName name

src/Halogen/VDom/StringRenderer.purs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ import Data.Array as A
99
import Data.Maybe (Maybe, maybe)
1010
import Data.String as S
1111
import Data.Tuple (snd)
12-
1312
import Halogen.VDom (VDom(..), ElemName(..), Namespace(..), runGraft)
14-
import Halogen.VDom.StringRenderer.Util (escape)
13+
import Halogen.VDom.StringRenderer.Util (cleanAndEscapeTextNode, cleanNonXnvName, escapeHtmlEntity)
1514

1615
-- | Type used to determine whether an element can be rendered as self-closing
1716
-- | element, for example, "<br/>".
@@ -35,7 +34,7 @@ render getTagType renderAttrs renderWidget = go
3534
where
3635
go VDom attrs widget String
3736
go = case _ of
38-
Text s → escape s
37+
Text s → cleanAndEscapeTextNode s
3938
Elem namespace elementName attrs children → renderElement namespace elementName attrs children
4039
Keyed namespace elementName attrs kchildren → renderElement namespace elementName attrs (map snd kchildren)
4140
Widget widget → renderWidget widget
@@ -45,9 +44,9 @@ render getTagType renderAttrs renderWidget = go
4544
renderElement maybeNamespace elemName@(ElemName name) attrs children =
4645
let
4746
as = renderAttrs attrs
48-
as' = maybe as (\(Namespace ns) -> "xmlns=\"" <> escape ns <> "\"" <> if S.null as then "" else " " <> as) maybeNamespace
47+
as' = maybe as (\(Namespace ns) -> "xmlns=\"" <> escapeHtmlEntity ns <> "\"" <> if S.null as then "" else " " <> as) maybeNamespace
48+
name' = cleanNonXnvName name
4949
in
50-
"<" <> name <> (if S.null as' then "" else " ") <> as' <>
51-
if A.null children
52-
then if getTagType elemName == SelfClosingTag then "/>" else "></" <> name <> ">"
53-
else ">" <> S.joinWith "" (map go children) <> "</" <> name <> ">"
50+
"<" <> name' <> (if S.null as' then "" else " ") <> as' <>
51+
if A.null children then if getTagType elemName == SelfClosingTag then "/>" else "></" <> name' <> ">"
52+
else ">" <> S.joinWith "" (map go children) <> "</" <> name' <> ">"
Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,76 @@
1-
module Halogen.VDom.StringRenderer.Util (escape) where
1+
module Halogen.VDom.StringRenderer.Util where
22

33
import Prelude
4-
import Data.String.Regex (Regex, replace')
5-
import Data.String.Regex.Flags (global)
4+
5+
import Data.String.Regex (Regex, replace', replace)
6+
import Data.String.Regex.Flags (global, unicode)
67
import Data.String.Regex.Unsafe (unsafeRegex)
78

8-
escapeRegex Regex
9-
escapeRegex = unsafeRegex "[\\\"\\\'/&<>]" global
10-
11-
escapeChar String String
12-
escapeChar = case _ of
13-
"\"""&quot;"
14-
"'""&#39;"
15-
"/""&#x2F;"
16-
"&""&amp;"
17-
"<""&lt;"
18-
">""&gt;"
19-
ch → ch
20-
21-
escape String String
22-
escape = replace' escapeRegex (const <<< escapeChar)
9+
-- TODO: use https://github.com/mathiasbynens/he ?
10+
escapeHtmlEntity String String
11+
escapeHtmlEntity = replace' escapeRegex (const <<< escapeChar)
12+
where
13+
escapeRegex Regex
14+
escapeRegex = unsafeRegex """[&"'<>\t\n\r\/]""" global
15+
16+
escapeChar String String
17+
escapeChar = case _ of
18+
"&" -> "&amp;"
19+
"\"" -> "&quot;"
20+
"'""&#39;"
21+
"<" -> "&lt;"
22+
">" -> "&gt;"
23+
"\t" -> "&#x9;"
24+
"\n" -> "&#xA;"
25+
"\r" -> "&#xD;"
26+
"/""&#x2F;"
27+
ch → ch
28+
29+
-- just like https://github.com/jsdom/w3c-xmlserializer/blob/83115f8ecce8ed77a2a907c74407b2c671751463/lib/attributes.js#L24
30+
-- NOTE: it will not escape / in https://mywebsite.com (check this test https://github.com/jsdom/w3c-xmlserializer/blob/83115f8ecce8ed77a2a907c74407b2c671751463/test/test.js#L58-L70)
31+
escapeAttributeValue String String
32+
escapeAttributeValue = replace' escapeRegex (const <<< escapeChar)
33+
where
34+
escapeRegex Regex
35+
escapeRegex = unsafeRegex """[&"<>\t\n\r]""" (global <> unicode)
36+
37+
escapeChar String String
38+
escapeChar = case _ of
39+
"&" -> "&amp;"
40+
"\"" -> "&quot;"
41+
"<" -> "&lt;"
42+
">" -> "&gt;"
43+
"\t" -> "&#x9;"
44+
"\n" -> "&#xA;"
45+
"\r" -> "&#xD;"
46+
ch → ch
47+
48+
-- just like https://github.com/jsdom/xml-name-validator/blob/836f307eec81279d2b1655587892e38a1effe039/lib/xml-name-validator.js#L4
49+
-- but will just remove all chars that are not `name`
50+
--
51+
-- Where we use it in `<my-element my-attr="my-attr-value"></my-element>`? We use it to clean `my-element` and `my-attr`.
52+
-- Just like w3c-xmlserializer does for element names (https://github.com/jsdom/w3c-xmlserializer/blob/83115f8ecce8ed77a2a907c74407b2c671751463/lib/serialize.js#L178) and attribute names (https://github.com/jsdom/w3c-xmlserializer/blob/83115f8ecce8ed77a2a907c74407b2c671751463/lib/attributes.js#L112).
53+
cleanNonXnvName String String
54+
cleanNonXnvName = replace invalidCharsRegex ""
55+
where
56+
invalidCharsRegex = unsafeRegex """[^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]""" (global <> unicode)
57+
58+
-- just like https://github.com/jsdom/w3c-xmlserializer/blob/83115f8ecce8ed77a2a907c74407b2c671751463/lib/serialize.js#L155
59+
-- removes all non XML_CHAR characters and changes escapes &<>
60+
cleanAndEscapeTextNode String String
61+
cleanAndEscapeTextNode = replace' escapeRegex (const <<< escapeChar) <<< cleanInvalidChars
62+
where
63+
cleanInvalidChars :: String -> String
64+
cleanInvalidChars = replace invalidCharsRegex ""
65+
where
66+
invalidCharsRegex = unsafeRegex """[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]""" (global <> unicode)
67+
68+
escapeRegex Regex
69+
escapeRegex = unsafeRegex "[&<>]" global
70+
71+
escapeChar String String
72+
escapeChar = case _ of
73+
"&""&amp;"
74+
"<""&lt;"
75+
">""&gt;"
76+
ch → ch

test/Test/Main.purs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
module Test.Main where
2+
3+
import Prelude
4+
5+
import Data.Maybe (Maybe(..))
6+
import Data.Tuple (Tuple)
7+
import Data.Tuple.Nested ((/\))
8+
import Effect (Effect)
9+
import Halogen.VDom (VDom, ElemName(..))
10+
import Halogen.VDom as V
11+
import Halogen.VDom.DOM.Prop (Prop(..), PropValue, propFromBoolean, propFromInt, propFromNumber, propFromString)
12+
import Halogen.VDom.DOM.StringRenderer (render)
13+
import Test.Spec (describe, it)
14+
import Test.Spec.Assertions (shouldEqual)
15+
import Test.Spec.Reporter (consoleReporter)
16+
import Test.Spec.Runner.Node (runSpecAndExitProcess)
17+
import Unsafe.Coerce (unsafeCoerce)
18+
19+
attr a. String String Prop a
20+
attr key value = Attribute Nothing key value
21+
22+
class IsPropValue v where
23+
toPropValue :: v -> PropValue
24+
25+
instance isPropValueString :: IsPropValue String where
26+
toPropValue = propFromString
27+
28+
instance isPropValueBoolean :: IsPropValue Boolean where
29+
toPropValue = propFromBoolean
30+
31+
instance isPropValueInt :: IsPropValue Int where
32+
toPropValue = propFromInt
33+
34+
instance isPropValueNumber :: IsPropValue Number where
35+
toPropValue = propFromNumber
36+
37+
prop :: forall a v. IsPropValue v => String -> v -> Prop a
38+
prop key value = Property key (toPropValue value)
39+
40+
infixr 1 attr as :=
41+
infixr 1 prop as .=
42+
43+
elem a. String Array (Prop a) Array (VDom (Array (Prop a)) a) VDom (Array (Prop a)) a
44+
elem n a c = V.Elem Nothing (ElemName n) a (unsafeCoerce c)
45+
46+
keyed a. String Array (Prop a) Array (Tuple String (VDom (Array (Prop a)) a)) VDom (Array (Prop a)) a
47+
keyed n a c = V.Keyed Nothing (ElemName n) a (unsafeCoerce c)
48+
49+
text a. String VDom (Array (Prop Void)) a
50+
text a = V.Text a
51+
52+
main Effect Unit
53+
main = runSpecAndExitProcess [ consoleReporter ] do
54+
it "works" do
55+
let
56+
html VDom (Array (Prop Void)) Void
57+
html =
58+
elem "div" [ "className" .= "container", "id" := "root" ]
59+
[ elem "label" [ "htmlFor" .= "username" ]
60+
[ text "Username" ]
61+
, elem "input" [ "id" := "input" ] []
62+
, elem "a" [ "href" := "index" ] [ text "Inbox" ]
63+
, keyed "div" []
64+
[ "0" /\ elem "span" [] [ text "0" ]
65+
, "1" /\ elem "span" [] [ text "1" ]
66+
]
67+
]
68+
render absurd html `shouldEqual` """<div class="container" id="root"><label for="username">Username</label><input id="input"/><a href="index">Inbox</a><div><span>0</span><span>1</span></div></div>"""
69+
it "should not escape links in prop attrs" do
70+
let
71+
html VDom (Array (Prop Void)) Void
72+
html =
73+
elem "script"
74+
[ "async" := ""
75+
, "type" := "application/javascript"
76+
, "src" := "chunks/defaultVendors~main-063769ad5d827b791041.js"
77+
]
78+
[]
79+
render absurd html `shouldEqual` """<script async="" type="application/javascript" src="chunks/defaultVendors~main-063769ad5d827b791041.js"></script>"""
80+
it "but should escape links prop names" do
81+
let
82+
html VDom (Array (Prop Void)) Void
83+
html =
84+
elem "script"
85+
[ "application/javascript" := "type"
86+
, "chunks/defaultVendors~main-063769ad5d827b791041.js" := "src"
87+
]
88+
[]
89+
render absurd html `shouldEqual` """<script application&#x2F;javascript="type" chunks&#x2F;defaultVendors~main-063769ad5d827b791041.js="src"></script>"""
90+
it "but should render int, boolean, number props" do
91+
let
92+
html VDom (Array (Prop Void)) Void
93+
html =
94+
elem "input"
95+
[ "maxLength" .= 255 -- Int
96+
, "step" .= 0.5 -- Number
97+
, "disabled" .= true -- Boolean
98+
, "readonly" .= false -- Boolean
99+
]
100+
[]
101+
render absurd html `shouldEqual` """<input maxLength="255" step="0.5" disabled/>"""
102+
describe "escape chars" do
103+
let
104+
mkElem char = elem ("div" <> char <> "test") [ "attr" <> char <> "key" := "attr" <> char <> "val", "prop" <> char <> "key" .= "prop" <> char <> "val" ] []
105+
mkTest char expected = it char $ render absurd (mkElem char) `shouldEqual` expected
106+
107+
mkTest "\"" """<divtest attr&quot;key="attr&quot;val" prop&quot;key="prop&quot;val"></divtest>"""
108+
mkTest "'" """<divtest attr&#39;key="attr'val" prop&#39;key="prop'val"></divtest>"""
109+
mkTest "/" """<divtest attr&#x2F;key="attr/val" prop&#x2F;key="prop/val"></divtest>"""
110+
mkTest "\\" """<divtest attr\key="attr\val" prop\key="prop\val"></divtest>"""
111+
mkTest "`" """<divtest attr`key="attr`val" prop`key="prop`val"></divtest>"""
112+
mkTest "?" """<divtest attr?key="attr?val" prop?key="prop?val"></divtest>"""
113+
mkTest "!" """<divtest attr!key="attr!val" prop!key="prop!val"></divtest>"""
114+
mkTest "@" """<divtest attr@key="attr@val" prop@key="prop@val"></divtest>"""
115+
mkTest "#" """<divtest attr#key="attr#val" prop#key="prop#val"></divtest>"""
116+
mkTest "$" """<divtest attr$key="attr$val" prop$key="prop$val"></divtest>"""

0 commit comments

Comments
 (0)