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
type t = Utf_8 | Windows_1252
let utf_8_labels = [ "unicode-1-1-utf-8"; "unicode11utf8"; "unicode20utf8"; "utf-8"; "utf8"; "x-unicode20utf8" ]
let windows_1252_labels =
[
"ansi_x3.4-1968"; "ascii"; "cp1252"; "cp819"; "csisolatin1"; "ibm819"; "iso-8859-1"; "iso-ir-100"; "iso8859-1";
"iso88591"; "iso_8859-1"; "iso_8859-1:1987"; "l1"; "latin1"; "us-ascii"; "windows-1252"; "x-cp1252";
]
let of_label (label : string) : t option =
let label = String.lowercase_ascii (String.trim label) in
if List.mem label utf_8_labels then Some Utf_8
else if List.mem label windows_1252_labels then Some Windows_1252
else None
let rec find (s : string) (sub : string) (from : int) : int option =
let n = String.length sub in
if from + n > String.length s then None
else if String.sub s from n = sub then Some from
else find s sub (from + 1)
let value_after (s : string) (i : int) (stop : int) : string =
let i = ref (i + String.length "charset=") in
while !i < stop && s.[!i] = ' ' do incr i done;
let quote = if !i < stop && (s.[!i] = '"' || s.[!i] = '\'') then Some s.[!i] else None in
if quote <> None then incr i;
let start = !i in
let ends c = match quote with Some q -> c = q | None -> c = ' ' || c = ';' || c = '>' || c = '/' || c = '"' || c = '\'' in
while !i < stop && not (ends s.[!i]) do incr i done;
String.sub s start (!i - start)
let of_content_type (content_type : string) : t option =
let s = String.lowercase_ascii content_type in
match find s "charset=" 0 with Some i -> of_label (value_after s i (String.length s)) | None -> None
let of_meta (bytes : string) : t option =
let s = String.lowercase_ascii (String.sub bytes 0 (min 1024 (String.length bytes))) in
let rec from (i : int) : t option =
match find s "<meta" i with
| None -> None
| Some start -> (
let stop = match String.index_from_opt s start '>' with Some j -> j | None -> String.length s in
match find s "charset=" start with
| Some i when i < stop -> (
match of_label (value_after s i stop) with Some t -> Some t | None -> from stop)
| _ -> from stop)
in
from 0
let is_utf_8 (s : string) : bool =
let rec ok (i : int) : bool =
if i >= String.length s then true
else
let d = String.get_utf_8_uchar s i in
Uchar.utf_decode_is_valid d && ok (i + Uchar.utf_decode_length d)
in
ok 0
let has_bom (s : string) : bool = String.length s >= 3 && String.sub s 0 3 = "\xEF\xBB\xBF"
let is_ascii (s : string) : bool = String.for_all (fun c -> Char.code c < 0x80) s
let detect ?content_type (bytes : string) : t =
if has_bom bytes then Utf_8
else
match Option.bind content_type of_content_type with
| Some t -> t
| None -> (
match of_meta bytes with
| Some t -> t
| None -> if (not (is_ascii bytes)) && is_utf_8 bytes then Utf_8 else Windows_1252)
let windows_1252_high =
[|
0x20AC; 0x0081; 0x201A; 0x0192; 0x201E; 0x2026; 0x2020; 0x2021; 0x02C6; 0x2030; 0x0160; 0x2039; 0x0152; 0x008D;
0x017D; 0x008F; 0x0090; 0x2018; 0x2019; 0x201C; 0x201D; 0x2022; 0x2013; 0x2014; 0x02DC; 0x2122; 0x0161; 0x203A;
0x0153; 0x009D; 0x017E; 0x0178;
|]
let windows_1252 (byte : int) : int = if byte >= 0x80 && byte <= 0x9F then windows_1252_high.(byte - 0x80) else byte
let to_utf_8 (t : t) (bytes : string) : string =
let b = Buffer.create (String.length bytes) in
(match t with
| Windows_1252 -> String.iter (fun c -> Buffer.add_utf_8_uchar b (Uchar.of_int (windows_1252 (Char.code c)))) bytes
| Utf_8 ->
let rec go (i : int) =
if i < String.length bytes then (
let d = String.get_utf_8_uchar bytes i in
Buffer.add_utf_8_uchar b (Uchar.utf_decode_uchar d);
go (i + Uchar.utf_decode_length d))
in
go (if has_bom bytes then 3 else 0));
Buffer.contents b
let decode ?content_type (bytes : string) : string = to_utf_8 (detect ?content_type bytes) bytes