Skip to content

Commit

Permalink
implement an escape hatch for emitting attributes with non-standard n…
Browse files Browse the repository at this point in the history
…ames (#295)
  • Loading branch information
cemerick committed Mar 18, 2022
1 parent 905fd4e commit 83a42d1
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 24 deletions.
4 changes: 3 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

* Add support for `dialog` element and `onclose` attribute
(#301 by Julien Sagot)

* Add an escape hatch for emitting attributes with non-standard names
in jsx or ppx code (a leading `_` character on attribute name)
(#295 Chas @cemerick Emerick)
* Add support for `type` attribute on `<script>` elements
(#293 by Ulrik @ulrikstrid Strid and Chas @cemerick Emerick)

Expand Down
21 changes: 21 additions & 0 deletions docs/manual-wiki/jsx.wiki
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,27 @@ version in <<a_api project="js_of_ocaml" subproject="js_of_ocaml-tyxml" | Js_of_

In each case, simply open the module and the JSX syntax will use it.

==@@id="unsafe"@@ "Unsafe" attributes

Some Javascript libraries and frameworks depend upon HTML markup that includes non-standard
attributes, which tyxml would usually reject. When constructing elements using tyxml's API directly,
the available workaround is to use the <<a_api text="Unsafe"|val Html_sigs.T.Unsafe>> constructors.
The same relaxed semantics can be had when using JSX by prefixing non-standard attribute names
with a leading underscore.

So, while this will fail:
<<code language="ocaml"|
let button = <button hx_post="/clicked" hx_swap="outerHTML">Click Me</button>
>>

This will not:
<<code language="ocaml"|
let button = <button _hx_post="/clicked" _hx_swap="outerHTML">Click Me</button>
>>

Such underscore-prefixed attributes are presumed to be strings, and are constructed using e.g.
<<a_api text="Unsafe.string_attrib"|val Html_sigs.T.Unsafe.string_attrib>>.

==@@id="tips"@@ Tips

It can sometime be necessary to disable the JSX syntax. For that purpose, simply use the toogle `[@tyxml.jsx false]` to turn the JSX on and off.
21 changes: 21 additions & 0 deletions docs/manual-wiki/ppx.wiki
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,27 @@ let my_div = [%html "<div id="my_id"></div>"]
val my_div : [> Html_types.div ] Html.elt
>>

==@@id="unsafe"@@ "Unsafe" attributes

Some Javascript libraries and frameworks depend upon HTML markup that includes non-standard
attributes, which tyxml would usually reject. When constructing elements using tyxml's API directly,
the available workaround is to use the <<a_api text="Unsafe"|val Html_sigs.T.Unsafe>> constructors.
The same relaxed semantics can be had when using the ppx by prefixing non-standard attribute names
with a leading underscore.

So, while this will fail:
<<code language="ocaml"|
[%html {|<button hx_post="/clicked" hx_swap="outerHTML">Click Me</button>|}]
>>

This will not:
<<code language="ocaml"|
[%html {|<button _hx_post="/clicked" _hx_swap="outerHTML">Click Me</button>|}]
>>

Such underscore-prefixed attributes are presumed to be strings, and are constructed using e.g.
<<a_api text="Unsafe.string_attrib"|val Html_sigs.T.Unsafe.string_attrib>>.

==@@id="let"@@ Let notation

It is also possible to use the ppx with the ##let## notation:
Expand Down
2 changes: 1 addition & 1 deletion jsx/dune
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
-open Ppxlib
-w "-9"
))
)
)
36 changes: 20 additions & 16 deletions jsx/tyxml_jsx.ml
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,27 @@ end
let lowercase_lead s =
String.mapi (fun i c -> if i = 0 then Char.lowercase_ascii c else c) s

let to_kebab_case name =
let to_kebab_case =
let open Re in
let kebab string =
replace (Posix.compile_pat "[A-Z]") ~f:(fun g -> "-" ^ Group.get g 0) string
|> String.lowercase_ascii
|> replace_string (compile @@ char '_') ~by:"-" in
match exec_opt (Perl.compile_pat {|^(data_?|aria_?)(.+)|}) name with
| None ->
if name.[0] == '_'
(* need to keep the leading underscore, as that's what the syntax support keys
off of to know to use Unsafe.string_attrib *)
then "_" ^ kebab @@ String.sub name 1 (String.length name - 1)
else name
| Some g ->
let prefix = String.sub name 0 4 in
let suffix = kebab @@ Group.get g 2 in
prefix ^ (if suffix.[0] == '-' then "" else "-") ^ suffix
let camelPat = Posix.compile_pat "[A-Z]" in
let underscore = compile @@ char '_' in
let prefixes = Perl.compile_pat {|^(data_?|aria_?)(.+)|} in
fun name ->
let kebab string =
replace camelPat ~f:(fun g -> "-" ^ Group.get g 0) string
|> String.lowercase_ascii
|> replace_string underscore ~by:"-" in
match exec_opt prefixes name with
| None ->
if name.[0] == '_'
(* need to keep the leading underscore, as that's what the syntax support keys
off of to know to use Unsafe.string_attrib *)
then "_" ^ kebab @@ String.sub name 1 (String.length name - 1)
else name
| Some g ->
let prefix = String.sub name 0 4 in
let suffix = kebab @@ Group.get g 2 in
prefix ^ (if suffix.[0] == '-' then "" else "-") ^ suffix

let make_attr_name name =
let name =
Expand Down
20 changes: 14 additions & 6 deletions syntax/attributes.ml
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,23 @@ let parse loc (language, element_name) attributes =
labeled, e::regular
in

(* Check if this is a "data-foo" or "aria-foo" attribute. Parse the
attribute value, and accumulate it in the list of attributes passed
(* Check if this is a "data-foo", "aria-foo", or unsafe attribute. Parse
the attribute value, and accumulate it in the list of attributes passed
in ~a. *)
match parse_prefixed "data-" local_name,
parse_prefixed "aria-" local_name
parse_prefixed "aria-" local_name,
parse_prefixed "_" local_name
with
| Some tag, _ -> parse_prefixed_attribute tag "a_user_data"
| _, Some tag -> parse_prefixed_attribute tag "a_aria"
| None, None ->
| Some tag, _, _ -> parse_prefixed_attribute tag "a_user_data"
| _, Some tag, _ -> parse_prefixed_attribute tag "a_aria"
| _, _, Some tag ->
let identifier = Common.make ~loc language "Unsafe.string_attrib" in
let tag = Common.string loc tag in
let value = match value with
| Val s -> Common.string loc s
| Antiquot v -> v in
labeled, [%expr [%e identifier] [%e tag] [%e value]] :: regular
| None, None, None ->
let tyxml_name =
match Common.find test_renamed Reflected.renamed_attributes with
| Some (name, _, _) -> name
Expand Down
10 changes: 10 additions & 0 deletions test/test_jsx.re
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,16 @@ let attribs = (
// the Xml.W.nil is here to satisfy the internal structure of what the jsx ppx produces
[div(~a=[a_user_data("foo-bar", "baz")], Xml.W.nil ())],
),
( "arbitrary (unchecked) attributes via an escape hatch",
[<div _some_attr="value"/>],
// the Xml.W.nil is here to satisfy the internal structure of what the jsx ppx produces
[div(~a=[Unsafe.string_attrib("some-attr", "value")], Xml.W.nil ())],
),
( "arbitrary (unchecked) attributes via an escape hatch, with antiquotation",
[<div _some_attr={"val" ++ "ue"}/>],
// the Xml.W.nil is here to satisfy the internal structure of what the jsx ppx produces
[div(~a=[Unsafe.string_attrib("some-attr", "value")], Xml.W.nil ())],
),
]
),
);
Expand Down
6 changes: 6 additions & 0 deletions test/test_ppx.ml
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,12 @@ let attribs = "ppx attribs", HtmlTests.make Html.[
[[%html "<div data-foo='valfoo'></div>"]],
[div
~a:[a_user_data "foo" "valfoo"] []] ;

"arbitrary (unchecked) attributes via an escape hatch",
[[%html "<div _some-attr='value'></div>"]],
[div
~a:[Unsafe.string_attrib "some-attr" "value"] []]

]

let ns_nesting = "namespace nesting" , HtmlTests.make Html.[
Expand Down

0 comments on commit 83a42d1

Please sign in to comment.