├── .ocamlformat-ignore
├── test
├── pp.ml
├── test.ml
├── test.expected.ml
└── dune
├── .gitignore
├── bin
├── bin.ml
└── dune
├── src
├── dune
└── ppx_jsx_embed.ml
├── nix
├── shell.nix
├── default.nix
└── ci
│ └── test.nix
├── README.md
├── dune-project
├── .github
└── workflows
│ └── test.yml
├── .ocamlformat
├── ppx_jsx_embed.opam
├── flake.nix
├── LICENSE
└── flake.lock
/.ocamlformat-ignore:
--------------------------------------------------------------------------------
1 | test/*.expected.ml
2 |
--------------------------------------------------------------------------------
/test/pp.ml:
--------------------------------------------------------------------------------
1 | let () = Ppxlib.Driver.standalone ()
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | _esy
2 | _build
3 | .merlin
4 | *.install
5 |
--------------------------------------------------------------------------------
/bin/bin.ml:
--------------------------------------------------------------------------------
1 | let () = Ppxlib.Driver.run_as_ppx_rewriter ()
2 |
--------------------------------------------------------------------------------
/bin/dune:
--------------------------------------------------------------------------------
1 | (executable
2 | (package ppx_jsx_embed)
3 | (name bin)
4 | (public_name ppx_jsx_embed)
5 | (libraries ppx_jsx_embed))
6 |
--------------------------------------------------------------------------------
/src/dune:
--------------------------------------------------------------------------------
1 | (library
2 | (public_name ppx_jsx_embed)
3 | (kind ppx_rewriter)
4 | (libraries ppxlib reason)
5 | (preprocess
6 | (pps ppxlib.metaquot)))
7 |
--------------------------------------------------------------------------------
/nix/shell.nix:
--------------------------------------------------------------------------------
1 | { packages, ocamlPackages, lib, mkShell }:
2 |
3 | mkShell {
4 | inputsFrom = lib.attrValues packages;
5 | buildInputs = with ocamlPackages; [ merlin ocamlformat utop ];
6 | }
7 |
--------------------------------------------------------------------------------
/test/test.ml:
--------------------------------------------------------------------------------
1 | let[@react.component] x ~children = {%jsx|
2 |
3 | |}
4 |
5 | let[@react.component] x ~some_prop ~children =
6 | {%jsx|
7 |
8 | |}
9 |
--------------------------------------------------------------------------------
/test/test.expected.ml:
--------------------------------------------------------------------------------
1 | let x ~children = ((div ~id:"omg" ~children:[] ())[@JSX ])[@@react.component
2 | ]
3 | let x ~some_prop ~children =
4 | ((div ~some_prop ~other_prop:some_prop ~children:[] ())[@JSX ])[@@react.component
5 | ]
6 |
--------------------------------------------------------------------------------
/nix/default.nix:
--------------------------------------------------------------------------------
1 | { lib, ocamlPackages, doCheck ? false, nix-filter }:
2 |
3 | with ocamlPackages;
4 |
5 | buildDunePackage {
6 | pname = "ppx_jsx_embed";
7 | version = "0.0.0";
8 |
9 | src = with nix-filter; filter {
10 | root = ./..;
11 | include = [ "src" "test" "dune" "dune-project" "ppx_jsx_embed.opam" ];
12 | };
13 | propagatedBuildInputs = [ reason ppxlib ];
14 |
15 | inherit doCheck;
16 | }
17 |
--------------------------------------------------------------------------------
/test/dune:
--------------------------------------------------------------------------------
1 | (executable
2 | (name pp)
3 | (modules pp)
4 | (libraries ppx_jsx_embed ppxlib))
5 |
6 | (rule
7 | (targets test.actual.ml)
8 | (deps
9 | (:pp pp.exe)
10 | (:input test.ml))
11 | (action
12 | (run ./%{pp} --impl %{input} -o %{targets})))
13 |
14 | (rule
15 | (alias runtest)
16 | (deps
17 | (file test.expected.ml)
18 | (file test.actual.ml))
19 | (action
20 | (diff test.expected.ml test.actual.ml)))
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ppx_jsx_embed
2 |
3 | `ppx_jsx_embed` allows embedding of Reason JSX within `.ml` files.
4 |
5 | ## Installation
6 |
7 | Install the library and its dependencies via [OPAM][opam]:
8 |
9 | [opam]: http://opam.ocaml.org/
10 |
11 | ```bash
12 | opam install ppx_jsx_embed
13 | ```
14 |
15 | ## Usage
16 |
17 | ```ocaml
18 | let[@react.component] my_component ~children = {%jsx|
19 |
20 | |}
21 | ```
22 |
23 | ## License
24 |
25 | ppx_jsx_embed is distributed under the 3-Clause BSD License, see
26 | [LICENSE](./LICENSE).
27 |
28 |
--------------------------------------------------------------------------------
/dune-project:
--------------------------------------------------------------------------------
1 | (lang dune 2.0)
2 |
3 | (name ppx_jsx_embed)
4 |
5 | (generate_opam_files true)
6 |
7 | (source
8 | (github melange/ppx_jsx_embed))
9 |
10 | (license BSD-3-clause)
11 |
12 | (maintainers "Antonio Monteiro ")
13 |
14 | (authors "Antonio Monteiro ")
15 |
16 | (package
17 | (name ppx_jsx_embed)
18 | (synopsis "A preprocessor extension for embedding Reason JSX in OCaml")
19 | (description
20 | "Ppx_jsx_embed allows using Reason JSX inside `{%jsx| |}`")
21 | (depends
22 | (ocaml
23 | (>= "4.11"))
24 | reason
25 | (ppxlib
26 | (>= "0.22"))))
27 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: "Build / Test"
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - master
7 | jobs:
8 | tests:
9 | runs-on: ubuntu-latest
10 | strategy:
11 | matrix:
12 | ocamlVersion: [4_12, 4_14, 5_0]
13 |
14 | steps:
15 | - uses: actions/checkout@v3
16 | - uses: cachix/install-nix-action@v20
17 | with:
18 | extra_nix_config: |
19 | extra-substituters = https://anmonteiro.nix-cache.workers.dev
20 | extra-trusted-public-keys = ocaml.nix-cache.com-1:/xI2h2+56rwFfKyyFVbkJSeGqSIYMC/Je+7XXqGKDIY=
21 | - name: "Run nix-build"
22 | run: nix-build ./nix/ci/test.nix --argstr ocamlVersion ${{ matrix.ocamlVersion }}
23 |
--------------------------------------------------------------------------------
/.ocamlformat:
--------------------------------------------------------------------------------
1 | break-infix = fit-or-vertical
2 | break-infix-before-func = false
3 | break-fun-decl = fit-or-vertical
4 | break-separators = before
5 | break-sequences = true
6 | cases-exp-indent = 2
7 | dock-collection-brackets = false
8 | field-space = loose
9 | if-then-else = keyword-first
10 | indicate-multiline-delimiters = no
11 | infix-precedence = parens
12 | leading-nested-match-parens = true
13 | let-and = sparse
14 | let-module = sparse
15 | ocp-indent-compat = true
16 | parens-tuple = multi-line-only
17 | parse-docstrings = true
18 | sequence-blank-line = preserve-one
19 | sequence-style = terminator
20 | single-case = sparse
21 | space-around-arrays= true
22 | space-around-lists= true
23 | space-around-records= true
24 | space-around-variants= true
25 | type-decl = sparse
26 | wrap-comments = true
27 | wrap-fun-args = false
28 |
29 |
--------------------------------------------------------------------------------
/ppx_jsx_embed.opam:
--------------------------------------------------------------------------------
1 | # This file is generated by dune, edit dune-project instead
2 | opam-version: "2.0"
3 | synopsis: "A preprocessor extension for embedding Reason JSX in OCaml"
4 | description:
5 | "Ppx_jsx_embed allows using Reason JSX inside `{%jsx| |}`"
6 | maintainer: ["Antonio Monteiro "]
7 | authors: ["Antonio Monteiro "]
8 | license: "BSD-3-clause"
9 | homepage: "https://github.com/melange/ppx_jsx_embed"
10 | bug-reports: "https://github.com/melange/ppx_jsx_embed/issues"
11 | depends: [
12 | "dune" {>= "2.0"}
13 | "ocaml" {>= "4.11"}
14 | "reason"
15 | "ppxlib" {>= "0.22"}
16 | ]
17 | build: [
18 | ["dune" "subst"] {pinned}
19 | [
20 | "dune"
21 | "build"
22 | "-p"
23 | name
24 | "-j"
25 | jobs
26 | "@install"
27 | "@runtest" {with-test}
28 | "@doc" {with-doc}
29 | ]
30 | ]
31 | dev-repo: "git+https://github.com/melange/ppx_jsx_embed.git"
32 |
--------------------------------------------------------------------------------
/nix/ci/test.nix:
--------------------------------------------------------------------------------
1 | { ocamlVersion }:
2 |
3 | let
4 | flake = builtins.getFlake (builtins.unsafeDiscardStringContext ./../..);
5 | system = builtins.currentSystem;
6 | pkgs = flake.inputs.nixpkgs.legacyPackages."${system}".extend (self: super: {
7 | ocamlPackages = super.ocaml-ng."ocamlPackages_${ocamlVersion}";
8 | });
9 |
10 | lock = builtins.fromJSON (builtins.readFile ./../../flake.lock);
11 | nix-filter-src = builtins.fetchGit {
12 | url = with lock.nodes.nix-filter.locked; "https://github.com/${owner}/${repo}";
13 | inherit (lock.nodes.nix-filter.locked) rev;
14 | # inherit (lock.nodes.nixpkgs.original) ref;
15 | allRefs = true;
16 | };
17 | nix-filter = import "${nix-filter-src}";
18 |
19 | inherit (pkgs) stdenv ocamlPackages callPackage;
20 |
21 | pkg = callPackage ./.. {
22 | doCheck = false;
23 | inherit nix-filter;
24 | };
25 | in
26 |
27 | stdenv.mkDerivation {
28 | name = "ppx_jsx_embed-tests";
29 | src = ./../..;
30 |
31 | dontBuild = true;
32 |
33 | installPhase = ''
34 | touch $out
35 | '';
36 |
37 | nativeBuildInputs = with ocamlPackages; [ ocaml dune findlib ocamlformat ];
38 | buildInputs = [ pkg ];
39 |
40 | doCheck = true;
41 | checkPhase = ''
42 | # Check code is formatted with OCamlformat
43 | dune build @fmt
44 | '';
45 | }
46 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Piaf Nix Flake";
3 |
4 | inputs.nix-filter.url = "github:numtide/nix-filter";
5 | inputs.flake-utils.url = "github:numtide/flake-utils";
6 | inputs.nixpkgs.inputs.flake-utils.follows = "flake-utils";
7 | inputs.nixpkgs.url = "github:nix-ocaml/nix-overlays";
8 |
9 | outputs = { self, nixpkgs, flake-utils, nix-filter }:
10 | flake-utils.lib.eachDefaultSystem (system:
11 | let
12 | pkgs = nixpkgs.legacyPackages."${system}".extend (self: super: {
13 | ocamlPackages = super.ocaml-ng.ocamlPackages_5_0.overrideScope' (oself: osuper: {
14 | reason = osuper.reason.overrideAttrs (o: {
15 | src = super.fetchFromGitHub {
16 | owner = "reasonml";
17 | repo = "reason";
18 | rev = "6401d10f2d1e2c8e1973c0de61d3c27d70b37248";
19 | hash = "sha256-QJORWjqGuz6pLhZTFLhWxofDsa4dAquVtwhdbGrv0pE=";
20 | };
21 | propagatedBuildInputs = o.propagatedBuildInputs ++ (with oself; [ dune-build-info ppxlib ]);
22 | });
23 | });
24 | });
25 | in
26 | rec {
27 | packages.default = pkgs.callPackage ./nix {
28 | nix-filter = nix-filter.lib;
29 | };
30 | devShells.default = pkgs.callPackage ./nix/shell.nix { inherit packages; };
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 António Nuno Monteiro
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its contributors
16 | may be used to endorse or promote products derived from this software without
17 | specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1681202837,
9 | "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "cfacdce06f30d2b68473a46042957675eebb3401",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "nix-filter": {
22 | "locked": {
23 | "lastModified": 1681154353,
24 | "narHash": "sha256-MCJ5FHOlbfQRFwN0brqPbCunLEVw05D/3sRVoNVt2tI=",
25 | "owner": "numtide",
26 | "repo": "nix-filter",
27 | "rev": "f529f42792ade8e32c4be274af6b6d60857fbee7",
28 | "type": "github"
29 | },
30 | "original": {
31 | "owner": "numtide",
32 | "repo": "nix-filter",
33 | "type": "github"
34 | }
35 | },
36 | "nixpkgs": {
37 | "inputs": {
38 | "flake-utils": [
39 | "flake-utils"
40 | ],
41 | "nixpkgs": "nixpkgs_2"
42 | },
43 | "locked": {
44 | "lastModified": 1682194439,
45 | "narHash": "sha256-hxI7ApCw19VIieOJ4tWewxPFZtAsOf0FLEOyM2VqDnc=",
46 | "owner": "nix-ocaml",
47 | "repo": "nix-overlays",
48 | "rev": "41eb189de4a02046291296e65784fe37fb1fea5e",
49 | "type": "github"
50 | },
51 | "original": {
52 | "owner": "nix-ocaml",
53 | "repo": "nix-overlays",
54 | "type": "github"
55 | }
56 | },
57 | "nixpkgs_2": {
58 | "locked": {
59 | "lastModified": 1682148940,
60 | "narHash": "sha256-IDJ768cBZgvX+mC1/ZGCpMBfSWMrw+OtG1luIZfMWjA=",
61 | "owner": "NixOS",
62 | "repo": "nixpkgs",
63 | "rev": "f362b87edfca438b3d2e03e68e66a9d09d30bb29",
64 | "type": "github"
65 | },
66 | "original": {
67 | "owner": "NixOS",
68 | "repo": "nixpkgs",
69 | "rev": "f362b87edfca438b3d2e03e68e66a9d09d30bb29",
70 | "type": "github"
71 | }
72 | },
73 | "root": {
74 | "inputs": {
75 | "flake-utils": "flake-utils",
76 | "nix-filter": "nix-filter",
77 | "nixpkgs": "nixpkgs"
78 | }
79 | },
80 | "systems": {
81 | "locked": {
82 | "lastModified": 1681028828,
83 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
84 | "owner": "nix-systems",
85 | "repo": "default",
86 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
87 | "type": "github"
88 | },
89 | "original": {
90 | "owner": "nix-systems",
91 | "repo": "default",
92 | "type": "github"
93 | }
94 | }
95 | },
96 | "root": "root",
97 | "version": 7
98 | }
99 |
--------------------------------------------------------------------------------
/src/ppx_jsx_embed.ml:
--------------------------------------------------------------------------------
1 | (*----------------------------------------------------------------------------
2 | * Copyright (c) 2021 António Nuno Monteiro
3 | *
4 | * All rights reserved.
5 | *
6 | * Redistribution and use in source and binary forms, with or without
7 | * modification, are permitted provided that the following conditions are met:
8 | *
9 | * 1. Redistributions of source code must retain the above copyright notice,
10 | * this list of conditions and the following disclaimer.
11 | *
12 | * 2. Redistributions in binary form must reproduce the above copyright
13 | * notice, this list of conditions and the following disclaimer in the
14 | * documentation and/or other materials provided with the distribution.
15 | *
16 | * 3. Neither the name of the copyright holder nor the names of its
17 | * contributors may be used to endorse or promote products derived from this
18 | * software without specific prior written permission.
19 | *
20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30 | * POSSIBILITY OF SUCH DAMAGE.
31 | *---------------------------------------------------------------------------*)
32 |
33 | open Ppxlib
34 | module RE = Reason_toolchain.RE
35 |
36 | let setup_lexbuf_from_string ~loc ~parser source =
37 | try
38 | let lexbuf = Lexing.from_string source in
39 | (* Sets the position of the lexing buffer to be the one at the start of the
40 | quoted extension, so that we can get precise error messages. *)
41 | Lexing.set_position lexbuf loc.loc_start;
42 | parser lexbuf
43 | with
44 | | Reason_errors.Reason_error _ as rexn -> raise rexn
45 | | Sys_error _ as exn ->
46 | (* file doesn't exist *)
47 | raise exn
48 | | _ -> failwith "NYI: different error"
49 |
50 | let parse_reason_impl omp_ast =
51 | let omp_ast =
52 | Reason_syntax_util.(
53 | apply_mapper_to_structure
54 | omp_ast
55 | (backport_letopt_mapper remove_stylistic_attrs_mapper))
56 | in
57 | (* Downside of Reason vendoring its own migrate_parsetree is this double
58 | copy. *)
59 | Ppxlib.Selected_ast.Of_ocaml.copy_structure
60 | (Reason_toolchain.To_current.copy_structure omp_ast)
61 |
62 | let parse_implementation_source ~loc source =
63 | let omp_ast =
64 | setup_lexbuf_from_string ~loc ~parser:RE.implementation source
65 | in
66 | parse_reason_impl omp_ast
67 |
68 | let jsx_rule =
69 | let expand ~loc ~path:_ reason_source =
70 | match parse_implementation_source ~loc reason_source with
71 | | [ { pstr_desc = Pstr_eval (reason_expression, _); _ } ] ->
72 | reason_expression
73 | | _ -> assert false
74 | in
75 | let extension =
76 | Extension.declare
77 | "jsx"
78 | Extension.Context.expression
79 | Ast_pattern.(single_expr_payload (estring __))
80 | expand
81 | in
82 | Context_free.Rule.extension extension
83 |
84 | let extension = Extension.declare
85 | let () = Driver.register_transformation ~rules:[ jsx_rule ] "jsx_embed"
86 |
--------------------------------------------------------------------------------