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
(** Simple CSS parser implementation. *)
type t = {
input : string;
len : int;
enforce_spec : bool;
mutable pos : int;
mutable call_stack : string list;
}
type parse_error = {
message : string;
got : string option;
position : int;
filename : string;
line : int;
col : int;
context_window : string;
marker_pos : int;
callstack : string list;
}
(** Parse error information with structured details. *)
exception Parse_error of parse_error
(** Pretty print parse error with debugging information *)
let pp_parse_error (err : parse_error) =
let buf = Buffer.create 256 in
Buffer.add_string buf err.message;
Buffer.add_string buf " at ";
Buffer.add_string buf err.filename;
Buffer.add_char buf ':';
Buffer.add_string buf (string_of_int err.line);
Buffer.add_char buf ':';
Buffer.add_string buf (string_of_int err.col);
(match err.callstack with
| [] -> ()
| callstack ->
Buffer.add_string buf "\n [stack: ";
Buffer.add_string buf (String.concat " -> " callstack);
Buffer.add_char buf ']');
if err.context_window <> "" then begin
let lines = String.split_on_char '\n' err.context_window in
let marker_line, marker_pos, _ =
Common.String.marker_line lines err.marker_pos
in
List.iteri
(fun i line ->
Buffer.add_char buf '\n';
Buffer.add_string buf line;
if i = marker_line then begin
Buffer.add_char buf '\n';
Buffer.add_string buf (String.make marker_pos ' ');
Buffer.add_char buf '^'
end)
lines
end;
Buffer.contents buf
let pp (ctx : Pp.ctx) (t : t) =
Pp.string ctx "<reader pos=";
Pp.string ctx (string_of_int t.pos);
Pp.string ctx ">"
(** {1 Creation} *)
let has_bom input =
let len = String.length input in
len >= 3 && input.[0] = '\xEF' && input.[1] = '\xBB' && input.[2] = '\xBF'
let rec needs_preprocess input len i =
i < len
&&
match input.[i] with
| '\x00' | '\r' | '\x0C' -> true
| _ -> needs_preprocess input len (i + 1)
let copy_preprocessed input len start =
let buf = Buffer.create len in
let fffd = "\xEF\xBF\xBD" in
let i = ref start in
while !i < len do
let c = input.[!i] in
(match c with
| '\x00' -> Buffer.add_string buf fffd
| '\r' ->
Buffer.add_char buf '\n';
if !i + 1 < len && input.[!i + 1] = '\n' then incr i
| '\x0C' -> Buffer.add_char buf '\n'
| _ -> Buffer.add_char buf c);
incr i
done;
Buffer.contents buf
let preprocess input =
let len = String.length input in
let bom = has_bom input in
let start = if bom then 3 else 0 in
if (not bom) && not (needs_preprocess input len 0) then input
else copy_preprocessed input len start
let of_string ?(enforce_spec = false) input =
let input = preprocess input in
{ input; len = String.length input; enforce_spec; pos = 0; call_stack = [] }
let source t = t.input
let enforce_spec t = t.enforce_spec
let is_done t = t.pos >= t.len
let peek_utf8_at t offset =
if offset < 0 || offset >= t.len - t.pos then None
else
let p = t.pos + offset in
let b = Char.code (String.unsafe_get t.input p) in
if b < 0x80 then Some (b, 1)
else
let len = min 4 (t.len - p) in
match Common.String.utf8_decode ~pos:p ~len t.input with
| Some (Common.String.Scalar u) ->
Some (Uchar.to_int u, Uchar.utf_8_byte_length u)
| Some (Common.String.Malformed _) | None -> None
let peek_utf8 t = peek_utf8_at t 0
let skip_utf8 t =
match peek_utf8 t with
| None -> if t.pos < t.len then t.pos <- t.pos + 1
| Some (_, n) -> t.pos <- t.pos + n
(** {1 Call Stack Management} *)
let push_context t context = t.call_stack <- context :: t.call_stack
let pop_context t =
match t.call_stack with
| [] -> ()
| _ :: rest -> t.call_stack <- rest
let with_context t context f =
push_context t context;
match f () with
| result ->
pop_context t;
result
| exception exn ->
pop_context t;
raise exn
let callstack t = List.rev t.call_stack
let context_window ?(before = 40) ?(after = 40) t =
let pos = t.pos in
let start_pos =
Common.String.utf8_lead_before t.input (max 0 (pos - before))
in
let end_pos =
Common.String.utf8_lead_after t.input (min t.len (pos + after))
in
let context = String.sub t.input start_pos (end_pos - start_pos) in
let marker_pos =
Common.String.utf8_length ~pos:start_pos ~len:(pos - start_pos) t.input
in
(context, marker_pos)
(** Error helpers *)
let err ?got t expected =
let context, marker_pos = context_window t in
let line, col =
Common.String.utf8_fold ~len:t.pos
(fun (line, col) _ decoded ->
match decoded with
| Common.String.Scalar u when Uchar.to_int u = 0x0A -> (line + 1, 1)
| Common.String.Scalar _ | Common.String.Malformed _ -> (line, col + 1))
(1, 1) t.input
in
raise
(Parse_error
{
message = expected;
got;
position = t.pos;
filename = "<CSS input>";
line;
col;
context_window = context;
marker_pos;
callstack = callstack t;
})
let err_eof t = err t "unexpected end of input"
let err_expected t what = err t ("expected " ^ what)
let err_expected_but_eof t what =
err t ("Expected " ^ what ^ " but reached end of input")
(** {1 Error Utilities} *)
let err_invalid t what = err t ("invalid " ^ what)
let with_filename error filename = { error with filename }
(** {1 Looking Ahead} *)
let peek t = if t.pos >= t.len then None else Some t.input.[t.pos]
let peek_at t offset =
let p = t.pos + offset in
if p < 0 || p >= t.len then None else Some (String.unsafe_get t.input p)
let peek_byte t =
if t.pos >= t.len then -1 else Char.code (String.unsafe_get t.input t.pos)
let peek_byte_at t offset =
let p = t.pos + offset in
if p < 0 || p >= t.len then -1 else Char.code (String.unsafe_get t.input p)
let peek_string t n =
let n = min n (t.len - t.pos) in
String.sub t.input t.pos n
let looking_at t s =
let slen = String.length s in
if t.pos + slen > t.len then false
else
let i = ref 0 in
let ok = ref true in
while !ok && !i < slen do
if String.unsafe_get t.input (t.pos + !i) <> String.unsafe_get s !i then
ok := false
else incr i
done;
!ok
(** {1 Reading Characters} *)
let skip t =
if t.pos >= t.len then err_eof t;
t.pos <- t.pos + 1
(** Get current position in input *)
let position t = t.pos
(** Get context window around current position for better error messages *)