├── .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 | --------------------------------------------------------------------------------