├── example_footer.html ├── .gitignore ├── example_main.ml ├── example.html ├── Makefile ├── README.md ├── LICENSE └── insideout.mll /example_footer.html: -------------------------------------------------------------------------------- 1 |

2 | This is the footer! 3 |

4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.cm* 2 | *.annot 3 | *.[oa] 4 | insideout.ml 5 | insideout 6 | -------------------------------------------------------------------------------- /example_main.ml: -------------------------------------------------------------------------------- 1 | let main () = 2 | print_string (Example.gen ~name: "User" ~num: 1 ()) 3 | 4 | let () = main () 5 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ${title:Welcome!} 5 | 6 | 7 |

${ title :Welcome!}

8 |

9 | Hello ${name}! 10 | You are visitor number ${num %i}. 11 |

12 |

13 | dollar open-curly a b c close-curly = \${abc} 14 |
15 | dollar open-curly a b c close-curly = ${x 16 | :${a\ 17 | b\ 18 | c\}} 19 |

20 | ${@example_footer.html} 21 | 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | insideout: insideout.mll 2 | ocamllex $< 3 | ocamlopt -o $@ insideout.ml 4 | 5 | .PHONY: demo 6 | demo: example 7 | ./insideout example.html -preview -o preview.html 8 | ./example > out.html 9 | 10 | example: example.ml example_main.ml 11 | ocamlopt -o example example.ml example_main.ml 12 | 13 | example.ml: insideout example.html 14 | ./insideout example.html -o example.ml 15 | 16 | ifndef PREFIX 17 | PREFIX = $(HOME) 18 | endif 19 | 20 | ifndef BINDIR 21 | BINDIR = $(PREFIX)/bin 22 | endif 23 | 24 | .PHONY: install 25 | install: 26 | cp insideout $(BINDIR) 27 | 28 | .PHONY: clean 29 | clean: 30 | rm -f *.o *.cm* *~ insideout.ml insideout 31 | rm -f example.ml example preview.html out.html 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | insideout 2 | ========= 3 | 4 | Description 5 | ----------- 6 | 7 | Convert a template into an OCaml function with labelled arguments. 8 | 9 | This is a camlp4-free replacement of the "interpolate file" feature of 10 | [xstrp4](http://projects.camlcity.org/projects/xstrp4.html). 11 | 12 | ``` 13 | Hello ${name} 14 | ``` 15 | 16 | becomes: 17 | 18 | ```ocaml 19 | let gen ~name = "Hello " ^ name 20 | ``` 21 | Use a backslash character to escape $ or \ itself (\\\${x} gives \${x}). 22 | 23 | Printf formatting: 24 | ``` 25 | ${x %f} 26 | ``` 27 | (formats the argument `x` using `Printf.sprintf "%f" x`) 28 | 29 | File inclusion (as-is, no escaping, no substitutions): 30 | ``` 31 | ${@foo/bar} 32 | ``` 33 | (includes the contents of file `foo/bar`) 34 | 35 | Default values (useful for previews or suggested usage): 36 | ``` 37 | ${title:Here goes the title} 38 | ``` 39 | 40 | Use `insideout -help` to see the command-line options. 41 | 42 | 43 | Installation 44 | ------------ 45 | 46 | (requires a standard OCaml installation) 47 | 48 | ``` 49 | $ make 50 | $ make install 51 | ``` 52 | 53 | Example 54 | ------- 55 | 56 | See file `example.html` and run demo with: 57 | ``` 58 | $ make demo 59 | ``` 60 | 61 | TODO 62 | ---- 63 | 64 | * add support for search path for included files 65 | * support inclusion of templates (as opposed to verbatim inclusion 66 | already supported) 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Mr. Number 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. The name of the author may not be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /insideout.mll: -------------------------------------------------------------------------------- 1 | { 2 | 3 | open Printf 4 | 5 | type var = { 6 | ident : string; 7 | format : string option; (* "%d" *) 8 | default : string option; 9 | } 10 | 11 | type token = 12 | Var of var 13 | | Text of string 14 | 15 | let pos1 lexbuf = lexbuf.Lexing.lex_start_p 16 | let pos2 lexbuf = lexbuf.Lexing.lex_curr_p 17 | let loc lexbuf = (pos1 lexbuf, pos2 lexbuf) 18 | 19 | let string_of_loc (pos1, pos2) = 20 | let open Lexing in 21 | let line1 = pos1.pos_lnum 22 | and start1 = pos1.pos_bol in 23 | sprintf "File %S, line %i, characters %i-%i" 24 | pos1.pos_fname line1 25 | (pos1.pos_cnum - start1) 26 | (pos2.pos_cnum - start1) 27 | 28 | let error lexbuf msg = 29 | eprintf "%s:\n%s\n%!" (string_of_loc (loc lexbuf)) msg; 30 | failwith "Aborted" 31 | 32 | let read_file lexbuf fname = 33 | try 34 | let ic = open_in fname in 35 | let len = in_channel_length ic in 36 | let s = String.create len in 37 | really_input ic s 0 len; 38 | s 39 | with e -> 40 | error lexbuf 41 | (sprintf "Cannot include file %s: %s" fname (Printexc.to_string e)) 42 | } 43 | 44 | let blank = [' ' '\t'] 45 | let space = [' ' '\t' '\r' '\n'] 46 | let ident = ['a'-'z']['a'-'z' '_' 'A'-'Z' '0'-'9']* 47 | let graph = ['\033'-'\126'] 48 | let format_char = graph # ['\\' ':' '}'] 49 | let filename = [^'}']+ 50 | 51 | rule tokens = parse 52 | | "${" space* (ident as ident) space* 53 | { let format = opt_format lexbuf in 54 | let default = opt_default lexbuf in 55 | Var { ident; format; default } 56 | :: tokens lexbuf 57 | } 58 | | "${@" (filename as filename) "}" 59 | (* as-is inclusion, no substitutions, 60 | no escaping *) 61 | { let s = read_file lexbuf filename in 62 | Text s :: tokens lexbuf } 63 | | "\\$" { Text "$" :: tokens lexbuf } 64 | | "\\\\" { Text "\\" :: tokens lexbuf } 65 | | [^'$''\\']+ as s { Text s :: tokens lexbuf } 66 | | _ as c { Text (String.make 1 c) :: tokens lexbuf } 67 | | eof { [] } 68 | 69 | and opt_format = parse 70 | | "%" format_char+ as format space* { Some format } 71 | | "" { None } 72 | 73 | and opt_default = parse 74 | | ":" { Some (string [] lexbuf) } 75 | | "}" { None } 76 | 77 | and string acc = parse 78 | | "}" { String.concat "" (List.rev acc) } 79 | | "\\\\" { string ("\\" :: acc) lexbuf } 80 | | "\\}" { string ("}" :: acc) lexbuf } 81 | | "\\\n" blank* { string acc lexbuf } 82 | | [^'}' '\\']+ as s { string (s :: acc) lexbuf } 83 | | _ as c { string (String.make 1 c :: acc) lexbuf } 84 | 85 | { 86 | open Printf 87 | 88 | let escape s = 89 | let buf = Buffer.create (2 * String.length s) in 90 | for i = 0 to String.length s - 1 do 91 | match s.[i] with 92 | | '$' -> Buffer.add_string buf "\\$" 93 | | '\\' -> Buffer.add_string buf "\\\\" 94 | | c -> Buffer.add_char buf c 95 | done; 96 | Buffer.contents buf 97 | 98 | let error source msg = 99 | eprintf "Error in file %s: %s\n%!" source msg; 100 | exit 1 101 | 102 | let parse_template source ic oc = 103 | let lexbuf = Lexing.from_channel ic in 104 | let l = tokens lexbuf in 105 | let tbl = Hashtbl.create 10 in 106 | List.iter ( 107 | function 108 | | Var x -> 109 | let id = x.ident in 110 | (try 111 | let x0 = Hashtbl.find tbl id in 112 | if x <> x0 then 113 | error source ( 114 | sprintf 115 | "Variable %s occurs multiple times with a \n\ 116 | different %%format or different default value." 117 | id 118 | ) 119 | else 120 | Hashtbl.replace tbl id x 121 | with Not_found -> 122 | Hashtbl.add tbl id x 123 | ) 124 | | Text _ -> 125 | () 126 | ) l; 127 | tbl, l 128 | 129 | let emit_ocaml use_defaults function_name source oc var_tbl l = 130 | let vars = 131 | List.sort 132 | (fun a b -> String.compare a.ident b.ident) 133 | (Hashtbl.fold (fun k v acc -> v :: acc) var_tbl []) 134 | in 135 | 136 | fprintf oc "(* Auto-generated from %s. Do not edit. *)\n" source; 137 | List.iter ( 138 | fun x -> 139 | match x.default with 140 | None -> () 141 | | Some s -> fprintf oc "let default_%s = %S\n" x.ident s 142 | ) vars; 143 | 144 | let args = 145 | let l = 146 | List.map ( 147 | function 148 | | { ident; default = Some default } when use_defaults -> 149 | sprintf "\n ?(%s = default_%s)" ident ident 150 | | { ident } -> 151 | "\n ~" ^ ident 152 | ) vars 153 | in 154 | String.concat "" l 155 | in 156 | fprintf oc "\ 157 | let %s%s () = 158 | 159 | String.concat \"\" [\n" 160 | function_name 161 | args; 162 | List.iter ( 163 | function 164 | | Var x -> 165 | let id = x.ident in 166 | (match x.format with 167 | | None -> 168 | fprintf oc " %s;\n" id 169 | | Some fmt -> 170 | fprintf oc " Printf.sprintf %S %s;\n" fmt id 171 | ) 172 | | Text s -> 173 | fprintf oc " %S;\n" s 174 | ) l; 175 | fprintf oc " ]\n" 176 | 177 | let expand_defaults esc oc l = 178 | List.iter ( 179 | function 180 | | Var x -> 181 | let id = x.ident in 182 | (match x.default with 183 | None -> 184 | let opt_format = 185 | match x.format with 186 | None -> "" 187 | | Some s -> " " ^ s 188 | in 189 | fprintf oc "${%s%s}" id opt_format 190 | | Some s -> 191 | output_string oc (if esc then escape s else s) 192 | ) 193 | | Text s -> 194 | output_string oc (if esc then escape s else s) 195 | ) l 196 | 197 | let ocaml use_defaults function_name source ic oc = 198 | let var_tbl, l = parse_template source ic oc in 199 | emit_ocaml use_defaults function_name source oc var_tbl l 200 | 201 | let preview esc function_name source ic oc = 202 | let var_tbl, l = parse_template source ic oc in 203 | expand_defaults esc oc l 204 | 205 | let main () = 206 | let out_file = ref None in 207 | let in_file = ref None in 208 | let function_name = ref "gen" in 209 | let use_defaults = ref true in 210 | let mode = ref `Ocaml in 211 | let options = [ 212 | "-f", 213 | Arg.Set_string function_name, 214 | " 215 | Name of the OCaml function (default: gen)"; 216 | 217 | "-o", 218 | Arg.String ( 219 | fun s -> 220 | if !out_file <> None then 221 | failwith "Multiple output files" 222 | else 223 | out_file := Some s 224 | ), 225 | " 226 | Output file (default: output goes to stdout)"; 227 | 228 | "-no-defaults", 229 | Arg.Clear use_defaults, 230 | " 231 | Produce an OCaml function with only required arguments, ignoring 232 | the defaults found in the template."; 233 | 234 | "-preview", 235 | Arg.Unit (fun () -> mode := `Preview), 236 | " 237 | Preview mode: substitute variables which have a default value, 238 | leave others intact."; 239 | 240 | "-xdefaults", 241 | Arg.Unit (fun () -> mode := `Xdefaults), 242 | " 243 | Expand the defaults like in -preview mode but produce a valid 244 | template, keeping special characters escaped."; 245 | ] 246 | in 247 | let anon_fun s = 248 | if !in_file <> None then 249 | failwith "Multiple input files" 250 | else 251 | in_file := Some s 252 | in 253 | 254 | let usage_msg = sprintf "\ 255 | Usage: %s [input file] [options] 256 | 257 | Convert a template into an OCaml function with labeled arguments. 258 | 259 | Hello ${x} 260 | 261 | becomes: 262 | 263 | let gen ~x () = \"Hello \" ^ x 264 | 265 | Use a backslash character to escape $ or \\ itself (\\\\\\${x} gives \\${x}). 266 | 267 | Also supported are %% format strings (as supported by OCaml's Printf): 268 | 269 | You are user number ${num %%i}. 270 | 271 | Finally, default values can be specified after a colon: 272 | 273 | ${title:Welcome} to our user number ${ num %%i :1234}! 274 | 275 | Command-line options: 276 | " 277 | Sys.argv.(0) 278 | in 279 | 280 | Arg.parse options anon_fun usage_msg; 281 | 282 | let ic, source = 283 | match !in_file with 284 | None -> stdin, "" 285 | | Some file -> open_in file, file 286 | in 287 | let oc = 288 | match !out_file with 289 | None -> stdout 290 | | Some file -> open_out file 291 | in 292 | match !mode with 293 | `Ocaml -> ocaml !use_defaults !function_name source ic oc 294 | | `Preview -> preview false !function_name source ic oc 295 | | `Xdefaults -> preview true !function_name source ic oc 296 | 297 | let () = main () 298 | } 299 | --------------------------------------------------------------------------------