Source file parser.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
open Handlebars_lexer

let ( let* ) = Result.bind
let ( >>= ) = Result.bind

let equal_pos a b =
  a.Lexing.pos_cnum = b.Lexing.pos_cnum
  && a.Lexing.pos_bol = b.Lexing.pos_bol
  && a.Lexing.pos_lnum = b.Lexing.pos_lnum

let equal_buf _ _ = true

type parse_error = {
  msg : string;
  pos : Lexing.position; [@equal equal_pos]
  buf : Lexing.lexbuf; [@equal equal_buf]
}
[@@deriving eq]

let string_of_path_segment segment =
  match segment with
  | `Ident name -> name
  | `DotPath `OneDot -> "."
  | `DotPath `TwoDot -> ".."
  | `Index (`String s) -> Printf.sprintf "['%s']" s
  | `Index (`Int i) -> Printf.sprintf "[%d]" i
  | _ -> failwith "not implemented"

let string_of_ident_path (path : Types.ident_path) =
  let aux path =
    let (`IdentPath segments) = path in
    match segments with
    | [] -> failwith "ident path shouldn't be empty"
    | _ ->
        segments
        |> List.fold_left
             (fun (prev, acc) this ->
               let s_this = string_of_path_segment this in
               match (prev, this) with
               | Some (`DotPath _), _ -> (Some this, acc ^ "/" ^ s_this)
               | Some _, `DotPath _ -> (Some this, acc ^ "/" ^ s_this)
               | Some _, `Ident _ -> (Some this, acc ^ "." ^ s_this)
               | _, _ -> (Some this, acc ^ s_this))
             (None, "")
  in
  let _, result = aux path in
  result

let string_of_literal lit : string =
  match lit with
  | `String s -> s
  | `Int i -> string_of_int i
  | _ -> failwith "not implemented"

let rec string_of_evalable (evalable : Types.evalable) =
  match evalable with
  | `IdentPath path -> string_of_ident_path (`IdentPath path)
  | `App (name, args) -> (
      match args with
      | [] -> name
      | _ ->
          let args_str =
            args |> List.map string_of_evalable |> String.concat ", "
          in
          Printf.sprintf "%s(%s)" name args_str)
  | `WhateverMakesSense exprs -> string_of_evalable (List.hd exprs)
  | `Literal l -> string_of_literal l

let mk_err msg buf : parse_error = { msg; pos = buf.Lexing.lex_curr_p; buf }

let pp_position fmt pos =
  Format.fprintf fmt "line %d, column %d" pos.Lexing.pos_lnum
    (pos.Lexing.pos_cnum - pos.Lexing.pos_bol)

let pp_parse_error fmt { msg; pos; _ } =
  pp_position fmt pos;
  Format.fprintf fmt ": %s" msg

let show_parse_error e =
  pp_parse_error Format.str_formatter e;
  Format.flush_str_formatter ()

type parse_result = (Types.token list, parse_error) result [@@deriving show, eq]

let mlex f buf = try Ok (f buf) with Failure msg -> Error (mk_err msg buf)

type container =
  | Root of Types.token list
  | Unclosed of { parent : container; block : Types.block }
  | UnclosedInverted of { parent : container; block : Types.block }

let mk_block ~kind evalable =
  { Types.expr = evalable; kind; content = []; else_content = [] }

let add_token container token =
  match container with
  | Root acc -> Root (token :: acc)
  | Unclosed { parent; block } ->
      let block = { block with content = token :: block.content } in
      Unclosed { parent; block }
  | UnclosedInverted { parent; block } ->
      let block = { block with else_content = token :: block.else_content } in
      UnclosedInverted { parent; block }

let ( ++ ) = add_token

let mature_unclosed container =
  match container with
  | Unclosed { parent; block } | UnclosedInverted { parent; block } ->
      let block =
        {
          block with
          content = List.rev block.content;
          else_content = List.rev block.else_content;
        }
      in
      parent ++ `Block block
  | _ -> failwith "Cannot mature a non-unclosed block"

let invert_unclosed container =
  match container with
  | Unclosed { parent; block } -> UnclosedInverted { parent; block }
  | UnclosedInverted _ ->
      failwith "This looks like an extra 'else' block in the template"
  | _ -> failwith "Cannot invert a non-unclosed block"

let parse_path_segment token : Types.ident_path_segment =
  match token with
  | IDENT id -> `Ident id
  | INDEX (`String s) -> `Index (`String s)
  | INDEX (`Int i) -> `Index (`Int i)
  | ONEDOT -> `DotPath `OneDot
  | TWODOT -> `DotPath `TwoDot

let mk_closing_path expr =
  let rec aux = function
    | `IdentPath path -> Some (`IdentPath path)
    | `App (name, _) -> Some (`IdentPath [ `Ident name ])
    | `WhateverMakesSense exprs ->
        let expr_seq = List.to_seq exprs in
        Seq.find_map aux expr_seq
    | _ -> None
  in
  match aux expr with
  | Some path -> path
  | None -> failwith "mk_closing_path: no path found in expr"

let rec parse_root container buf =
  let* lexres = mlex lex buf in
  match lexres with
  | EOF -> (
      match container with
      | Root acc -> Ok (List.rev acc)
      | Unclosed _ | UnclosedInverted _ ->
          Error (mk_err "unmatched block (did you forget an {{/}}?)" buf))
  | WHITESPACE s -> parse_root (container ++ `Whitespace s) buf
  | RAW s -> parse_root (container ++ `Raw s) buf
  | TEMPL_OPEN { ws_control; kind } -> (
      let container =
        if ws_control then container ++ `WhitespaceControl else container
      in
      match kind with
      | Escaped -> parse_escaped container buf
      | Unescaped -> parse_unescaped container buf
      | Partial ->
          let* partial_info, ws_control = parse_partial buf in
          let container = container ++ `Partial partial_info in
          let container =
            if ws_control then container ++ `WhitespaceControl else container
          in
          parse_root container buf
      | Section -> parse_section container buf
      | InvertedSection -> parse_inverted_section container buf
      | CloseSection -> parse_close_section container buf)
  | COMMENT -> parse_root (container ++ `Comment) buf
  | _ -> Error (mk_err "unexpected token in root" buf)

and parse_escaped container buf =
  let* inner, ws_control = parse_templ buf in
  let* container =
    match inner with
    | `App ("else", []) -> (
        try Ok (invert_unclosed container)
        with Failure e -> Error (mk_err e buf))
    | _ -> Ok (container ++ `Escaped inner)
  in
  let container =
    if ws_control then container ++ `WhitespaceControl else container
  in
  parse_root container buf

and parse_unescaped container buf =
  let* inner, ws_control = parse_templ ~is_unescaped:true buf in
  let container = container ++ `Unescaped inner in
  let container =
    if ws_control then container ++ `WhitespaceControl else container
  in
  parse_root container buf

and parse_templ ?(is_unescaped = false) buf =
  let expect_templ_close result = function
    | TEMPL_CLOSE { ws_control; is_unescaped = t_is_unescaped; raw } ->
        if is_unescaped <> t_is_unescaped then
          let expected =
            if is_unescaped then templ_open ^ String.make 1 templ_open_char
            else templ_open
          in
          let msg =
            Printf.sprintf "expected closing tag \"%s\" but found \"%s\""
              expected raw
          in
          Error (mk_err msg buf)
        else Ok (result, ws_control)
    | _ -> Error (mk_err "expected closing tag" buf)
  in
  mlex lex_in_templ buf >>= function
  | IDENT_PATH [ IDENT name ] when name = "else" ->
      let expr = `App ("else", []) in
      mlex lex_in_templ buf >>= expect_templ_close expr
  | IDENT_PATH [ IDENT name ]
  | IDENT_PATH [ INDEX (`String name) ]
  | LITERAL (`String name) ->
      let until = function TEMPL_CLOSE _ -> true | _ -> false in
      (* TODO: add support for hash args for helpers *)
      let* pos_args, _hash_args, close_token = parse_arguments ~until buf in
      let expr =
        match pos_args with
        | [] ->
            `WhateverMakesSense [ `App (name, []); `IdentPath [ `Ident name ] ]
        | _ -> `App (name, pos_args)
      in
      close_token |> expect_templ_close expr
  | LPAREN ->
      let* name, pos_args, _hash_args, _last =
        parse_application ~until:(equal_token RPAREN) buf
      in
      mlex lex_in_templ buf >>= expect_templ_close (`App (name, pos_args))
  | IDENT_PATH path ->
      let expr = `IdentPath (List.map parse_path_segment path) in
      mlex lex_in_templ buf >>= expect_templ_close expr
  | LITERAL (`Int i) ->
      let expr = `IdentPath [ `Index (`Int i) ] in
      mlex lex_in_templ buf >>= expect_templ_close expr
  | token ->
      let msg = Printf.sprintf "unexpected token: %s" (show_token token) in
      Error (mk_err msg buf)

and parse_application ~until buf =
  mlex lex_in_templ buf >>= function
  | IDENT_PATH [ IDENT name ]
  | IDENT_PATH [ INDEX (`String name) ]
  | LITERAL (`String name) ->
      let* pos_args, hash_args, last_token = parse_arguments ~until buf in
      Ok (name, pos_args, hash_args, last_token)
  | _ -> Error (mk_err "expected function name in application" buf)

and parse_arguments ~until buf =
  let parse_arg = function
    | IDENT_PATH path -> Ok (`IdentPath (List.map parse_path_segment path))
    | LITERAL lit -> Ok (`Literal lit)
    | LPAREN ->
        let* name, pos_args, _hash_args, _t =
          parse_application ~until:(equal_token RPAREN) buf
        in
        Ok (`App (name, pos_args))
    | token ->
        let msg =
          Printf.sprintf "unexpected token: \"%s\"" (show_token token)
        in
        Error (mk_err msg buf)
  in
  let rec aux pos_args hash_args buf =
    mlex lex_in_templ buf >>= function
    | t when until t -> Ok (List.rev pos_args, List.rev hash_args, t)
    | START_HASH_ARG name ->
        let* v = mlex lex_in_templ buf >>= parse_arg in
        aux pos_args ((name, v) :: hash_args) buf
    | t ->
        let* arg = parse_arg t in
        aux (arg :: pos_args) hash_args buf
  in
  aux [] [] buf

and parse_section container buf =
  let* evalable, ws_control = parse_templ buf in
  let block = mk_block ~kind:Section evalable in
  let child = Unclosed { parent = container; block } in
  let child = if ws_control then child ++ `WhitespaceControl else child in
  parse_root child buf

and parse_inverted_section container buf =
  let* evalable, ws_control = parse_templ buf in
  let block = mk_block ~kind:InvertedSection evalable in
  let child = UnclosedInverted { parent = container; block } in
  let child = if ws_control then child ++ `WhitespaceControl else child in
  parse_root child buf

and parse_close_section container buf =
  let* inner, ws_control = parse_templ buf in
  match container with
  | Root _ -> Error (mk_err "unexpected close block without matching open" buf)
  | Unclosed { block; _ } | UnclosedInverted { block; _ } -> (
      let expected_closing = mk_closing_path block.expr in
      let rec make_sense = function
        | `IdentPath path -> Some (`IdentPath path)
        | `WhateverMakesSense exprs ->
            let expr_seq = List.to_seq exprs in
            Seq.find_map make_sense expr_seq
        | _ -> None
      in
      match make_sense inner with
      | Some path when Types.equal_evalable path expected_closing ->
          let container = mature_unclosed container in
          let container =
            if ws_control then container ++ `WhitespaceControl else container
          in
          parse_root container buf
      | _ ->
          let msg =
            Printf.sprintf
              "unexpected close block: \"%s\"; does not match \"%s\""
              (string_of_evalable inner)
              (string_of_evalable expected_closing)
          in
          Error (mk_err msg buf))

and parse_partial buf =
  let until = function TEMPL_CLOSE _ -> true | _ -> false in
  let* name, pos_args, hash_args, last_token = parse_application ~until buf in
  let* ws_control =
    match last_token with
    | TEMPL_CLOSE { ws_control; _ } -> Ok ws_control
    | _ -> Error (mk_err "expected closing tag" buf)
  in
  let partial_info =
    match pos_args with
    | [] -> { Types.name; context = None; hash_args }
    | [ context ] -> { Types.name; context = Some context; hash_args }
    | _ -> failwith "A partial takes exactly 1 positional argument for context"
  in
  Ok (partial_info, ws_control)

let parse lexbuf : parse_result = parse_root (Root []) lexbuf