├── src_test ├── fail_dollar_at_end_of_string.ml ├── fail_invalid_var.ml ├── fail_invalid_var_name.ml ├── fail_no_closing_paren.ml ├── fail_unescaped_dollar.ml └── app.ml ├── .merlin ├── META ├── .gitignore ├── opam ├── readme.org ├── LICENCE.txt ├── Makefile └── src └── ppx_string_interpolate.ml /src_test/fail_dollar_at_end_of_string.ml: -------------------------------------------------------------------------------- 1 | open Printf 2 | 3 | let () = 4 | print_string [%str "hello $"]; 5 | print_newline() 6 | 7 | -------------------------------------------------------------------------------- /src_test/fail_invalid_var.ml: -------------------------------------------------------------------------------- 1 | open Printf 2 | 3 | let () = 4 | print_string [%str "hello $(name1234)"]; 5 | print_newline() 6 | 7 | -------------------------------------------------------------------------------- /src_test/fail_invalid_var_name.ml: -------------------------------------------------------------------------------- 1 | open Printf 2 | 3 | let () = 4 | print_string [%str "hello $(λ)"]; 5 | print_newline() 6 | 7 | -------------------------------------------------------------------------------- /src_test/fail_no_closing_paren.ml: -------------------------------------------------------------------------------- 1 | open Printf 2 | 3 | let () = 4 | print_string [%str "hello $(name"]; 5 | print_newline() 6 | 7 | -------------------------------------------------------------------------------- /src_test/fail_unescaped_dollar.ml: -------------------------------------------------------------------------------- 1 | open Printf 2 | 3 | let () = 4 | print_string [%str "hello $name"]; 5 | print_newline() 6 | 7 | -------------------------------------------------------------------------------- /.merlin: -------------------------------------------------------------------------------- 1 | S src 2 | S src_test 3 | 4 | B src 5 | B src_test 6 | 7 | PKG str 8 | PKG sedlex 9 | PKG sedlex.ppx 10 | PKG ppx_tools 11 | PKG ppx_tools.metaquot 12 | 13 | -------------------------------------------------------------------------------- /META: -------------------------------------------------------------------------------- 1 | description = "A simple ppx filter for string interpolation" 2 | version = "0.1" 3 | requires = "sedlex, ppx_tools, ppx_deriving" 4 | ppx = "./ppx_string_interpolate" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.annot 2 | *.cmi 3 | *.cmt 4 | *.cmx 5 | *.native 6 | *.o 7 | /src_test/*.out 8 | /src_test/*.test 9 | /build/ 10 | /src/ppx_string_interpolate 11 | /src_test/app 12 | -------------------------------------------------------------------------------- /src_test/app.ml: -------------------------------------------------------------------------------- 1 | open Printf 2 | 3 | let () = 4 | let name = "ppx_string" in 5 | let mood = "fine" in 6 | let larrys_baby = "perl" in 7 | 8 | let msg = [%str "hello $(name) are you $(mood)?\n"] in 9 | print_string msg; 10 | 11 | print_string [%str {eof| 12 | This also works with new string syntax 13 | So you can do templates like in $(larrys_baby). 14 | 15 | |eof}]; 16 | 17 | print_string [%str "testing double dollar: $$(name)"]; 18 | print_newline() 19 | 20 | -------------------------------------------------------------------------------- /opam: -------------------------------------------------------------------------------- 1 | opam-version: "1.2" 2 | maintainer: "Jan Rehders " 3 | authors: [ "Jan Rehders " ] 4 | license: "Public domain" 5 | version: "0.1" 6 | homepage: "https://github.com/sheijk/ppx_string_interpolate" 7 | bug-reports: "https://github.com/sheijk/ppx_string_interpolate/issues" 8 | dev-repo: "https://github.com/sheijk/ppx_string_interpolate.git" 9 | tags: [ "syntax" ] 10 | build: ["make" "all"] 11 | build-test: ["make" "test"] 12 | install: ["make" "install"] 13 | remove: ["make" "uninstall"] 14 | depends: [ 15 | "ppx_tools" {>= "0.99.2"} 16 | "sedlex" {>= "1.99.2"} 17 | ] 18 | available: [ocaml-version >= "4.02.1"] 19 | -------------------------------------------------------------------------------- /readme.org: -------------------------------------------------------------------------------- 1 | 2 | Simple string interpolation using OCaml's ppx extension points API. 3 | 4 | * How to use 5 | 6 | Write [%str "some string"] anywhere a string expression is expected. Use $(name) 7 | to insert the value of variable "name" whose types needs to be string. Use $$ to 8 | insert an actual dollar character. 9 | 10 | #+begin_src tuareg 11 | let name = "mario" in 12 | print_string [%str "It's a meee, $(name)\n"] 13 | #+end_src 14 | 15 | See directory src_test for examples on what to do and not to do. 16 | 17 | * How to build 18 | 19 | - install OCaml 4.02 (or newer) 20 | - install opam 1.1.1 (or newer) 21 | - install dependencies 22 | opam install sedlex ppx_tools ppx_deriving 23 | - make all 24 | 25 | Test by running 'make test' 26 | 27 | * Todo 28 | 29 | - support arbitrary OCaml expressions inside $(...). Needs to run the OCaml 30 | lexer on the string, skip over balanced parenthesis and pass this to OCaml 31 | Parse.expression. 32 | 33 | * Contact 34 | 35 | Mail to jan@sheijk.net for questions and comments. 36 | 37 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | PPX_STRING_PACKAGES = -package ppx_tools -package ppx_tools.metaquot -package sedlex 3 | OCAMLOPT_FLAGS += -bin-annot -annot 4 | 5 | .PHONY: all 6 | all: src/ppx_string_interpolate src_test/app 7 | 8 | src_test/%.cmx: src_test/%.ml 9 | ocamlfind ocamlopt -c -o $@ $< $(OCAMLOPT_FLAGS) $(PPX_STRING_PACKAGES) -ppx ./src/ppx_string_interpolate 10 | 11 | src_test/%: src_test/%.cmx src/ppx_string_interpolate 12 | ocamlfind ocamlopt -o $@ $< $(OCAMLOPT_FLAGS) -ppx ./src/ppx_string_interpolate -linkpkg 13 | 14 | %.cmx: %.ml 15 | ocamlfind ocamlopt -c -o $@ $< $(OCAMLOPT_FLAGS) $(PPX_STRING_PACKAGES) 16 | 17 | src/ppx_string_interpolate: src/ppx_string_interpolate.cmx 18 | ocamlfind ocamlopt -o $@ $< $(OCAMLOPT_FLAGS) $(PPX_STRING_PACKAGES) -linkpkg 19 | 20 | .PHONY: clean 21 | clean: 22 | rm -f {src,src_test}/*.{cmi,cmx,o} src/ppx_string_interpolate src_test/app 23 | $(foreach case, $(FAIL_CASES), rm -f src_test/fail_$(case).{out,test}) 24 | 25 | 26 | INSTALL_FILES = META src/ppx_string_interpolate 27 | 28 | .PHONY: install 29 | install: 30 | ocamlfind install ppx_string_interpolate $(INSTALL_FILES) 31 | 32 | .PHONY: uninstall 33 | uninstall: 34 | ocamlfind remove ppx_string_interpolate 35 | 36 | ################################################################################ 37 | # Tests 38 | 39 | FAIL_CASES = invalid_var invalid_var_name no_closing_paren unescaped_dollar dollar_at_end_of_string 40 | 41 | src_test/fail_%.test src_test/fail_%.out: src_test/fail_%.ml 42 | -(ocamlfind ocamlopt -o $@ $< -ppx ./src/ppx_string_interpolate 2>&1) > $(@:.test=.out) 43 | grep "File \"$(<)\", line 4" $(@:.test=.out) > /dev/null 44 | touch $@ 45 | 46 | .PHONY: test 47 | test: src_test/app $(foreach case, $(FAIL_CASES), src_test/fail_$(case).test) 48 | ./src_test/app 49 | 50 | $(BUILD_DIR)/.exists: 51 | mkdir -p $(BUILD_DIR) 52 | touch $@ 53 | 54 | ################################################################################ 55 | # Emacs flymake 56 | 57 | BUILD_DIR = build 58 | 59 | FLYMAKE_LOG=$(BUILD_DIR)/flymake-log.txt 60 | FLYMAKE_BUILD=$(BUILD_DIR)/flymake-last-build.txt 61 | 62 | .PHONY: flymake.ml.check 63 | flymake.ml.check: 64 | @(ocamlfind ocamlopt $(OCAMLOPT_FLAGS) $(PPX_STRING_PACKAGES) -c $(CHK_SOURCES) -o /tmp/flymake_temp.cmx 2>&1) | sed 's/_flymake//g' | tee $(FLYMAKE_BUILD) 65 | 66 | .PHONY: flymake.mli.check 67 | flymake.mli.check: flymake.ml.check 68 | 69 | .PHONY: flymake_log 70 | flymake_log: 71 | @echo "$(shell date '+%Y-%m-%d %H:%M:%S') checking $(CHK_SOURCES)" >> $(FLYMAKE_LOG) 72 | 73 | .PHONY: check-syntax 74 | check-syntax: $(BUILD_DIR)/.exists flymake_log flymake$(suffix $(CHK_SOURCES)).check 75 | 76 | -------------------------------------------------------------------------------- /src/ppx_string_interpolate.ml: -------------------------------------------------------------------------------- 1 | open Ast_mapper 2 | open Ast_helper 3 | open Asttypes 4 | open Parsetree 5 | open Longident 6 | 7 | type part = String of string | Var of string 8 | 9 | exception Parse_error of string * int 10 | 11 | let parse_string str = 12 | let parts = ref [] in 13 | let add item = 14 | parts := item :: !parts; 15 | in 16 | let buf = Sedlexing.Utf8.from_string str in 17 | let pos buf = 18 | let start, _ = Sedlexing.loc buf in 19 | start 20 | in 21 | let rec loop() = 22 | match%sedlex buf with 23 | | Plus (Compl '$') -> 24 | add @@ String (Sedlexing.Utf8.lexeme buf); 25 | loop() 26 | | "$$" -> 27 | add @@ String (Sedlexing.Utf8.lexeme buf); 28 | loop() 29 | | "$(", Plus (Compl ')'), ')' -> 30 | let token = Sedlexing.Utf8.lexeme buf in 31 | let var = String.sub token 2 (String.length token - 3) in 32 | add @@ Var var; 33 | loop() 34 | | ('$', Plus (Compl ('$' | '('))) | ('$', eof) -> 35 | raise (Parse_error ("Expected $ to be followed by '$' or '('", pos buf + 1)) 36 | | "$(", Plus (Compl ')'), eof -> 37 | raise (Parse_error ("Expected closing ')' but found end of string", String.length str)) 38 | | eof -> 39 | () 40 | | _ -> 41 | raise (Parse_error ("Unexpected character", pos buf)) 42 | in 43 | loop(); 44 | List.rev !parts 45 | 46 | let to_str_code parts = 47 | let to_str = function 48 | | String str -> 49 | Exp.constant ~loc:!(Ast_helper.default_loc) (Const_string (str, None)) 50 | | Var name -> 51 | Exp.ident @@ { txt = Longident.parse name; loc = !(Ast_helper.default_loc) } 52 | in 53 | [%expr String.concat "" [%e Ast_convenience.list (List.map to_str parts)]] 54 | 55 | let ppx_string_interpolate_mapper argv = 56 | (* Our ppx_string_interpolate_mapper only overrides the handling of expressions in the default mapper. *) 57 | { default_mapper with 58 | expr = fun mapper expr -> 59 | match expr with 60 | (* Is this an extension node? *) 61 | | { pexp_desc = 62 | (* Should have name "str". *) 63 | Pexp_extension ({ txt = "str"; loc }, pstr)} -> 64 | begin match pstr with 65 | | (* Should have a single structure item, which is evaluation of a constant string. *) 66 | PStr [{ pstr_desc = 67 | Pstr_eval ({ pexp_loc = loc; 68 | pexp_desc = Pexp_constant (Const_string (sym, _))}, _)}] -> 69 | begin 70 | try 71 | let parts = parse_string sym in 72 | Ast_helper.with_default_loc loc (fun () -> to_str_code parts) 73 | 74 | with Parse_error (message, pos) -> 75 | let string_start = loc.Location.loc_start.Lexing.pos_cnum in 76 | let loc = { loc with 77 | Location.loc_start = { 78 | loc.Location.loc_start with Lexing.pos_cnum = string_start + 1 + pos }; 79 | loc_end = { loc.Location.loc_end with Lexing.pos_cnum = string_start + 2 + pos } } 80 | in 81 | let error = Location.error ~loc ("Error: " ^ message) in 82 | raise (Location.Error error) 83 | end 84 | | _ -> 85 | raise (Location.Error ( 86 | Location.error ~loc "[%str] accepts a string, e.g. [%str \"USER\"]")) 87 | end 88 | (* Delegate to the default mapper. *) 89 | | x -> default_mapper.expr mapper x; 90 | } 91 | 92 | let () = register "str" ppx_string_interpolate_mapper 93 | 94 | --------------------------------------------------------------------------------