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