package virtual_dom

  1. Overview
  2. Docs

Source file css_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
(* Recursive descent parsers.  A parser returns false if based on a
   single token lookahead it decides that the given text can not be
   parsed.  Any other parse errors are handled by raising exceptions.

   Some parsers return unit because we only call them when a failure
   to parse implies a parse error (and not that some parser higher up
   in the call chain should try to parse something else).

   For simplicity the parsers themselves just validate and don't
   produce any values.  That leads to a few unelegant constructs
   (primarily in declaration), but means we otherwise have rather
   simple code that also allocates very little.
*)

open Core_kernel
open! Int.Replace_polymorphic_compare

let rec next ct =
  Css_tokenizer.next ct;
  if Css_tokenizer.(Token.equal (current ct) Comment) then next ct else ()
;;

let skip_white_space ct =
  while Css_tokenizer.(Token.equal (current ct) White_space) do
    next ct
  done
;;

let accept ct expected =
  let got = Css_tokenizer.current ct in
  if Css_tokenizer.Token.equal got expected
  then (
    next ct;
    true)
  else false
;;

let expect ct expected =
  let got = Css_tokenizer.current ct in
  if Css_tokenizer.Token.equal got expected
  then next ct
  else
    raise_s
      [%message
        "Unexpected token"
          (expected : Css_tokenizer.Token.t)
          (got : Css_tokenizer.Token.t)]
;;

let rec many (ct : Css_tokenizer.t) f = if f ct then many ct f else ()

let many1 (ct : Css_tokenizer.t) f =
  if f ct
  then (
    many ct f;
    true)
  else false
;;

let rec any ct : bool =
  let res =
    match Css_tokenizer.current ct with
    | Ident | Number | Percentage | Dimension | String | Uri | Delim | Hash ->
      next ct;
      true
    | Function ->
      next ct;
      skip_white_space ct;
      many ct any;
      expect ct Rparen;
      true
    | Lparen ->
      next ct;
      skip_white_space ct;
      expect_any ct;
      expect ct Rparen;
      true
    | Lbracket ->
      next ct;
      skip_white_space ct;
      expect_any ct;
      expect ct Rbracket;
      true
    | _ -> false
  in
  if res then skip_white_space ct else ();
  res

and expect_any ct = if any ct then () else raise_s [%message "Expected <any>"]

and value0 ct =
  any ct
  || block ct
  ||
  if accept ct Atkeyword
  then (
    skip_white_space ct;
    true)
  else false

and value ct = many1 ct value0

and block ct : bool =
  if accept ct Lcurly
  then (
    skip_white_space ct;
    many ct (fun ct ->
      value0 ct
      ||
      if accept ct Semi
      then (
        skip_white_space ct;
        true)
      else false);
    expect ct Rcurly;
    skip_white_space ct;
    true)
  else false

and expect_value ct = if value ct then () else raise_s [%message "Expected <value>"]

let declaration ct =
  let ident_start, ident_len = Css_tokenizer.slice ct in
  if accept ct Ident
  then (
    skip_white_space ct;
    expect ct Colon;
    skip_white_space ct;
    let value_start = Css_tokenizer.slice ct |> fst in
    expect_value ct;
    let next_token_start = Css_tokenizer.slice ct |> fst in
    let source = Css_tokenizer.source ct in
    Some
      ( String.sub source ~pos:ident_start ~len:ident_len
      , String.rstrip
          (String.sub source ~pos:value_start ~len:(next_token_start - value_start)) ))
  else None
;;

let expect_declaration ct =
  match declaration ct with
  | Some (field, value) -> field, value
  | None -> raise_s [%message "Expected <declaration>"]
;;

(* As per: https://www.w3.org/TR/css-style-attr/
   declaration-list
   : S* declaration? [ ';' S* declaration? ]*
   ;
*)
let expect_declaration_list ct =
  let res = ref [] in
  let add kv =
    match kv with
    | None -> ()
    | Some (k, v) -> res := (k, v) :: !res
  in
  skip_white_space ct;
  add (declaration ct);
  many ct (fun ct ->
    if accept ct Semi
    then (
      skip_white_space ct;
      add (declaration ct);
      true)
    else false);
  List.rev !res
;;

let parse parser_f s =
  let ct = Css_tokenizer.create s in
  while Css_tokenizer.(Token.equal (current ct) Comment) do
    Css_tokenizer.next ct
  done;
  Or_error.try_with (fun () ->
    let res = parser_f ct in
    expect ct Eof;
    res)
;;

let validate_value = parse expect_value
let parse_declaration_list s = parse expect_declaration_list s

let test_parser p sexp_of_arg s =
  let r = parse p s in
  printf !"%s --> %{sexp:arg Or_error.t}\n" s r
;;

let%expect_test "values" =
  let test = test_parser expect_value Unit.sexp_of_t in
  test "x";
  test "3";
  test "3in";
  test "3%";
  test "#fff";
  test "1 0 auto";
  test "'Hello World'";
  test "rgb(0,0,0)";
  [%expect
    {|
      x --> (Ok ())
      3 --> (Ok ())
      3in --> (Ok ())
      3% --> (Ok ())
      #fff --> (Ok ())
      1 0 auto --> (Ok ())
      'Hello World' --> (Ok ())
      rgb(0,0,0) --> (Ok ()) |}]
;;

let%expect_test "declaration" =
  let test = test_parser expect_declaration [%sexp_of: string * string] in
  test "flex: 1 0 auto";
  test "content: 'Hello World'";
  test "content: foo;";
  (* Semi's are handled in declaration list *)
  test "content: bar ";
  (* but whitespace is handled in declaration (any really) *)
  [%expect
    {|
      flex: 1 0 auto --> (Ok (flex "1 0 auto"))
      content: 'Hello World' --> (Ok (content "'Hello World'"))
      content: foo; --> (Error ("Unexpected token" (expected Eof) (got Semi)))
      content: bar  --> (Ok (content bar)) |}]
;;

let%expect_test "unicode" =
  let test = test_parser expect_declaration [%sexp_of: string * string] in
  test "content: '← ↑ → ↓ ↔ ↕ ⇪ ↹ ⬈ ↘ ⟾ ↶'";
  print_endline
    (Sexp.to_string (Sexp.Atom "← ↑ → ↓ ↔ ↕ ⇪ ↹ ⬈ ↘ ⟾ ↶"));
  [%expect
    {|
    content: '← ↑ → ↓ ↔ ↕ ⇪ ↹ ⬈ ↘ ⟾ ↶' --> (Ok
     (content
      "'\226\134\144 \226\134\145 \226\134\146 \226\134\147 \226\134\148 \226\134\149 \226\135\170 \226\134\185 \226\172\136 \226\134\152 \226\159\190 \226\134\182'"))
    "\226\134\144 \226\134\145 \226\134\146 \226\134\147 \226\134\148 \226\134\149 \226\135\170 \226\134\185 \226\172\136 \226\134\152 \226\159\190 \226\134\182" |}]
;;

let%expect_test "declaration list" =
  let test = test_parser expect_declaration_list [%sexp_of: (string * string) list] in
  test "flex: 1 0 auto";
  test "flex: 1 0 auto;";
  test "background: #5d9ab2 url(\"img_tree.png\") no-repeat top left;margin-left: 200px";
  test ";;;;;";
  test "flex: 1 0 auto ;; other : sa ";
  [%expect
    {|
    flex: 1 0 auto --> (Ok ((flex "1 0 auto")))
    flex: 1 0 auto; --> (Ok ((flex "1 0 auto")))
    background: #5d9ab2 url("img_tree.png") no-repeat top left;margin-left: 200px --> (Ok
     ((background "#5d9ab2 url(\"img_tree.png\") no-repeat top left")
      (margin-left 200px)))
    ;;;;; --> (Ok ())
    flex: 1 0 auto ;; other : sa  --> (Ok ((flex "1 0 auto") (other sa))) |}]
;;
OCaml

Innovation. Community. Security.