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
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
module Css = Cascade.Css
let str = String.concat ""
module El = struct
type html =
| Element of string * (string * string) list * html list
| Text of string
| Void
| Raw of string
let v ~at name children = Element (name, at, children)
let txt s = Text s
let void = Void
let unsafe_raw s = Raw s
let escape_html s =
let b = Buffer.create (String.length s) in
String.iter
(function
| '<' -> Buffer.add_string b "<"
| '>' -> Buffer.add_string b ">"
| '&' -> Buffer.add_string b "&"
| '"' -> Buffer.add_string b """
| '\'' -> Buffer.add_string b "'"
| c -> Buffer.add_char b c)
s;
Buffer.contents b
let rec to_string ?(doctype = false) = function
| Text s -> escape_html s
| Raw s -> s
| Void -> ""
| Element (name, attrs, children) ->
let b = Buffer.create 256 in
if doctype && name = "html" then Buffer.add_string b "<!DOCTYPE html>\n";
Buffer.add_char b '<';
Buffer.add_string b name;
List.iter
(fun (k, v) ->
Buffer.add_char b ' ';
Buffer.add_string b k;
Buffer.add_string b "=\"";
Buffer.add_string b (escape_html v);
Buffer.add_char b '"')
attrs;
if
children = []
&& List.mem name [ "img"; "br"; "hr"; "input"; "meta"; "link" ]
then Buffer.add_string b " />"
else (
Buffer.add_char b '>';
List.iter
(fun child -> Buffer.add_string b (to_string child))
children;
Buffer.add_string b "</";
Buffer.add_string b name;
Buffer.add_char b '>');
Buffer.contents b
end
module At = struct
type t = string * string
let v name value = (name, value)
let class' s = ("class", s)
let id s = ("id", s)
let title s = ("title", s)
let lang s = ("lang", s)
let dir s = ("dir", s)
let tabindex i = ("tabindex", string_of_int i)
let contenteditable b = ("contenteditable", if b then "true" else "false")
let spellcheck s = ("spellcheck", s)
let type' s = ("type", s)
let value s = ("value", s)
let name s = ("name", s)
let placeholder s = ("placeholder", s)
let required = ("required", "")
let disabled = ("disabled", "")
let checked = ("checked", "")
let href s = ("href", s)
let rel s = ("rel", s)
let src s = ("src", s)
let alt s = ("alt", s)
let width i = ("width", string_of_int i)
let height i = ("height", string_of_int i)
let charset s = ("charset", s)
let content s = ("content", s)
let style s = ("style", s)
let onclick s = v "onclick" s
let onchange s = v "onchange" s
let oninput s = v "oninput" s
let onsubmit s = v "onsubmit" s
let readonly = v "readonly" ""
let target s = v "target" s
let download s = v "download" s
let loading s = v "loading" s
let property s = v "property" s
let datetime s = v "datetime" s
let srcset s = v "srcset" s
let sizes s = v "sizes" s
let title' s = v "title" s
let loading_lazy = v "loading" "lazy"
let true' name = v name ""
let false' name = v name "false"
let void = ("", "")
let if' cond at = if cond then at else void
let if_some = function Some at -> at | None -> void
let is_void (name, _) = name = ""
let to_pair at = at
let of_pair p = p
let accesskey s = v "accesskey" s
let action s = v "action" s
let autocomplete s = v "autocomplete" s
let autofocus = v "autofocus" ""
let cols i = v "cols" (string_of_int i)
let colspan i = v "colspan" (string_of_int i)
let defer = v "defer" ""
let draggable b = v "draggable" (if b then "true" else "false")
let for' s = v "for" s
let hidden = v "hidden" ""
let list s = v "list" s
let method' s = v "method" s
let media s = v "media" s
let popover s = v "popover" s
let popovertarget s = v "popovertarget" s
let popovertargetaction s = v "popovertargetaction" s
let rows i = v "rows" (string_of_int i)
let rowspan i = v "rowspan" (string_of_int i)
let selected = v "selected" ""
let wrap s = v "wrap" s
let fill_rule `evenodd = v "fill-rule" "evenodd"
let clip_rule `evenodd = v "clip-rule" "evenodd"
let cx i = v "cx" (string_of_int i)
let cy i = v "cy" (string_of_int i)
let r i = v "r" (string_of_int i)
let view_box s = v "viewBox" s
let fill s = v "fill" s
let stroke s = v "stroke" s
let stroke_width s = v "stroke-width" s
let stroke_linecap s = v "stroke-linecap" s
let stroke_linejoin s = v "stroke-linejoin" s
let x s = v "x" s
let y s = v "y" s
let rx s = v "rx" s
let d s = v "d" s
let x1 s = v "x1" s
let y1 s = v "y1" s
let x2 s = v "x2" s
let y2 s = v "y2" s
end
type tw = Tw.t
type t = { el : El.html; tw : tw list; forms : bool }
type attr = At.t
module Aria = struct
let label s = At.v "aria-label" s
let labelledby s = At.v "aria-labelledby" s
let describedby s = At.v "aria-describedby" s
let hidden = At.v "aria-hidden" "true"
let expanded b = At.v "aria-expanded" (string_of_bool b)
let current s = At.v "aria-current" s
let role s = At.v "role" s
end
let to_htmlit t = t.el
let to_tw t = t.tw
let has_forms t = t.forms
let txt s = { el = El.txt s; tw = []; forms = false }
let txtf segments = txt (str segments)
let raw s = { el = El.unsafe_raw s; tw = []; forms = false }
let rawf segments = raw (str segments)
let empty = { el = El.void; tw = []; forms = false }
let class_atts tw_styles raw_classes other_atts =
let tw_cls = match tw_styles with [] -> "" | _ -> Tw.to_classes tw_styles in
let all_cls =
match (tw_cls, raw_classes) with
| "", [] -> ""
| s, [] -> s
| "", parts -> String.concat " " parts
| s, parts -> String.concat " " (s :: parts)
in
match all_cls with
| "" -> List.rev other_atts
| cls -> At.class' cls :: List.rev other_atts
let parse_class_value value =
let classes =
String.split_on_char ' ' value |> List.filter (fun s -> s <> "")
in
List.fold_left
(fun (tw_acc, raw_acc) cls ->
match Tw.of_string cls with
| Ok t -> (t :: tw_acc, raw_acc)
| Error _ -> (tw_acc, cls :: raw_acc))
([], []) classes
let atts =
List.fold_left
(fun (, raw_cls, rest) ((name, value) as att) ->
if name = "class" then
let tw_parsed, raw = parse_class_value value in
(List.rev tw_parsed @ tw_extra, List.rev raw @ raw_cls, rest)
else (tw_extra, raw_cls, att :: rest))
([], [], []) atts
let el_with_tw ?(forms = false) name ?at ?(tw = []) children =
let atts = Option.value ~default:[] at in
let tw_from_at, raw_classes, other_atts = extract_class_attrs atts in
let all_tw_styles = tw @ tw_from_at in
let atts_with_tw = class_atts all_tw_styles raw_classes other_atts in
let child_els = List.map to_htmlit children in
let all_tw = all_tw_styles @ List.concat_map to_tw children in
let has_forms = forms || List.exists (fun c -> c.forms) children in
{ el = El.v ~at:atts_with_tw name child_els; tw = all_tw; forms = has_forms }
let to_string ?(doctype = false) t = El.to_string ~doctype (to_htmlit t)
module Livereload = struct
let enabled =
try Sys.getenv "SITE_LIVERELOAD" = "true" with Not_found -> false
let endpoint =
try Sys.getenv "SITE_LIVERELOAD_ENDPOINT"
with Not_found -> "ws://localhost:8080"
let script =
if enabled then
raw
(str
[
"<script>\n";
"(function() {\n";
" const ws = new WebSocket('";
endpoint;
"');\n";
" ws.onmessage = (event) => {\n";
" if (event.data === 'reload') {\n";
" location.reload();\n";
" }\n";
" };\n";
"})();\n";
"</script>";
])
else empty
end
let div ?at ?tw children = el_with_tw "div" ?at ?tw children
let span ?at ?tw children = el_with_tw "span" ?at ?tw children
let p ?at ?tw children = el_with_tw "p" ?at ?tw children
let a ?at ?tw children = el_with_tw "a" ?at ?tw children
let ul ?at ?tw children = el_with_tw "ul" ?at ?tw children
let li ?at ?tw children = el_with_tw "li" ?at ?tw children
let nav ?at ?tw children = el_with_tw "nav" ?at ?tw children
let section ?at ?tw children = el_with_tw "section" ?at ?tw children
let article ?at ?tw children = el_with_tw "article" ?at ?tw children
let ?at ?tw children = el_with_tw "header" ?at ?tw children
let ?at ?tw children = el_with_tw "footer" ?at ?tw children
let h1 ?at ?tw children = el_with_tw "h1" ?at ?tw children
let h2 ?at ?tw children = el_with_tw "h2" ?at ?tw children
let h3 ?at ?tw children = el_with_tw "h3" ?at ?tw children
let h4 ?at ?tw children = el_with_tw "h4" ?at ?tw children
let h5 ?at ?tw children = el_with_tw "h5" ?at ?tw children
let h6 ?at ?tw children = el_with_tw "h6" ?at ?tw children
let script ?at ?tw children = el_with_tw "script" ?at ?tw children
let title ?at ?tw children = el_with_tw "title" ?at ?tw children
let head ?at ?tw children = el_with_tw "head" ?at ?tw children
let body ?at ?tw children = el_with_tw "body" ?at ?tw children
let root ?at ?tw children = el_with_tw "html" ?at ?tw children
let option ?at ?tw children = el_with_tw "option" ?at ?tw children
let select ?at ?tw children = el_with_tw ~forms:true "select" ?at ?tw children
let main ?at ?tw children = el_with_tw "main" ?at ?tw children
let aside ?at ?tw children = el_with_tw "aside" ?at ?tw children
let time ?at ?tw children = el_with_tw "time" ?at ?tw children
let dialog ?at ?tw children = el_with_tw "dialog" ?at ?tw children
let data ?at ?tw children = el_with_tw "data" ?at ?tw children
let picture ?at ?tw children = el_with_tw "picture" ?at ?tw children
let slot ?at ?tw children = el_with_tw "slot" ?at ?tw children
let template ?at ?tw children = el_with_tw "template" ?at ?tw children
let void_el ?(forms = false) name ?at ?(tw = []) () =
let atts = Option.value ~default:[] at in
let tw_from_at, raw_classes, other_atts = extract_class_attrs atts in
let all_tw = tw @ tw_from_at in
let atts_with_tw = class_atts all_tw raw_classes other_atts in
{ el = El.v ~at:atts_with_tw name []; tw = all_tw; forms }
let img ?at ?tw () = void_el "img" ?at ?tw ()
let meta ?at ?tw () = void_el "meta" ?at ?tw ()
let link ?at ?tw () = void_el "link" ?at ?tw ()
let style ?at ?(tw = []) css =
let atts = Option.value ~default:[] at in
let tw_from_at, raw_classes, other_atts = extract_class_attrs atts in
let all_tw = tw @ tw_from_at in
let atts_with_tw = class_atts all_tw raw_classes other_atts in
{
el = El.v ~at:atts_with_tw "style" [ El.unsafe_raw css ];
tw = all_tw;
forms = false;
}
let void = empty
let form ?at ?tw children = el_with_tw "form" ?at ?tw children
let input ?at ?tw () = void_el ~forms:true "input" ?at ?tw ()
let textarea ?at ?tw children =
el_with_tw ~forms:true "textarea" ?at ?tw children
let button ?at ?tw children = el_with_tw "button" ?at ?tw children
let label ?at ?tw children = el_with_tw "label" ?at ?tw children
let fieldset ?at ?tw children = el_with_tw "fieldset" ?at ?tw children
let legend ?at ?tw children = el_with_tw "legend" ?at ?tw children
let details ?at ?tw children = el_with_tw "details" ?at ?tw children
let summary ?at ?tw children = el_with_tw "summary" ?at ?tw children
let pre ?at ?tw children = el_with_tw "pre" ?at ?tw children
let code ?at ?tw children = el_with_tw "code" ?at ?tw children
let kbd ?at ?tw children = el_with_tw "kbd" ?at ?tw children
let em ?at ?tw children = el_with_tw "em" ?at ?tw children
let strong ?at ?tw children = el_with_tw "strong" ?at ?tw children
let small ?at ?tw children = el_with_tw "small" ?at ?tw children
let mark ?at ?tw children = el_with_tw "mark" ?at ?tw children
let br ?at ?tw () = void_el "br" ?at ?tw ()
let hr ?at ?tw () = void_el "hr" ?at ?tw ()
let table ?at ?tw children = el_with_tw "table" ?at ?tw children
let thead ?at ?tw children = el_with_tw "thead" ?at ?tw children
let tbody ?at ?tw children = el_with_tw "tbody" ?at ?tw children
let ?at ?tw children = el_with_tw "tfoot" ?at ?tw children
let tr ?at ?tw children = el_with_tw "tr" ?at ?tw children
let th ?at ?tw children = el_with_tw "th" ?at ?tw children
let td ?at ?tw children = el_with_tw "td" ?at ?tw children
let ol ?at ?tw children = el_with_tw "ol" ?at ?tw children
let dl ?at ?tw children = el_with_tw "dl" ?at ?tw children
let dt ?at ?tw children = el_with_tw "dt" ?at ?tw children
let dd ?at ?tw children = el_with_tw "dd" ?at ?tw children
let blockquote ?at ?tw children = el_with_tw "blockquote" ?at ?tw children
let figure ?at ?tw children = el_with_tw "figure" ?at ?tw children
let figcaption ?at ?tw children = el_with_tw "figcaption" ?at ?tw children
let video ?at ?tw children = el_with_tw "video" ?at ?tw children
let audio ?at ?tw children = el_with_tw "audio" ?at ?tw children
let source ?at ?tw () = void_el "source" ?at ?tw ()
let canvas ?at ?tw children = el_with_tw "canvas" ?at ?tw children
let iframe ?at ?tw children = el_with_tw "iframe" ?at ?tw children
let svg ?at ?tw children = el_with_tw "svg" ?at ?tw children
let g ?at ?tw children = el_with_tw "g" ?at ?tw children
let circle ?at ?tw children = el_with_tw "circle" ?at ?tw children
let rect ?at ?tw children = el_with_tw "rect" ?at ?tw children
let path ?at ?tw children = el_with_tw "path" ?at ?tw children
let line ?at ?tw children = el_with_tw "line" ?at ?tw children
type tw_css = Link of string | Inline
type page = { html : string; css : Tw.Css.t; tw_css : tw_css }
let page_impl ~lang ~meta_list ?title_text ~charset ~tw_css ?forms head_content
body_content =
let meta_charset = meta ~at:[ At.charset charset ] () in
let meta_tags =
meta_charset
:: List.map
(fun (name, content) ->
meta ~at:[ At.name name; At.content content ] ())
meta_list
in
let body_element = body body_content in
let all_tw = to_tw body_element in
let all_tw = all_tw @ List.concat_map to_tw head_content in
let css_stylesheet =
match forms with
| Some forms -> Tw.to_css ~forms all_tw
| None -> Tw.to_css all_tw
in
let css_string = Tw.Css.to_string ~minify:true css_stylesheet in
let css_head =
match tw_css with
| Inline -> style css_string
| Link file ->
let css_hash =
let digest = Digest.string css_string in
String.sub (Digest.to_hex digest) 0 8
in
let css_url_with_hash = String.concat "" [ file; "?v="; css_hash ] in
link ~at:[ At.rel "stylesheet"; At.href css_url_with_hash ] ()
in
let head_children =
meta_tags
@ (match title_text with Some t -> [ title [ txt t ] ] | None -> [])
@ [ css_head ] @ head_content
in
let html_tree =
root ~at:[ At.lang lang ] [ head head_children; body_element ]
in
let html_string = to_string ~doctype:true html_tree in
{ html = html_string; css = css_stylesheet; tw_css }
let page ?(lang = "en") ?(meta = []) ?title ?(charset = "utf-8")
?(tw_css = Link "tw.css") ?forms head_content body_content =
page_impl ~lang ~meta_list:meta ?title_text:title ~charset ~tw_css ?forms
head_content body_content
let html page = page.html
let css page =
match page.tw_css with
| Link file -> (Some file, page.css)
| Inline -> (None, page.css)
let pp t =
let tw_classes = Tw.to_classes t.tw in
let el_str = El.to_string ~doctype:false t.el in
if tw_classes = "" then el_str
else
String.concat ""
[ "<element with classes=\""; tw_classes; "\">"; el_str; "</element>" ]