Source file frontmatter.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
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
type resolved = [ `Resolved ]
type unresolved = [ `Unresolved ]
type 'a fm = {
toplevel_attributes : Cmarkit.Attributes.t option;
math_link : 'a option;
theme : [ `Builtin of Themes.t | `External of string ] option;
css_links : 'a list;
js_links : 'a list;
dimension : (int * int) option;
highlightjs_theme : string option;
math_mode : [ `Mathjax | `Katex ] option;
external_ids : string list;
}
(** We keep an option even though there are default value to be able to merge
two frontmatter. None and default value represent different things. *)
module Toplevel_attributes = struct
type t = Cmarkit.Attributes.t
let key = "toplevel-attributes"
let default =
Cmarkit.Attributes.make
~kv_attributes:
[
(("slip", Cmarkit.Meta.none), None);
( ("enter", Cmarkit.Meta.none),
Some ({ v = "~duration:0"; delimiter = None }, Cmarkit.Meta.none) );
]
()
let of_string (s, loc) =
let s = String.trim s in
let s =
if String.length s > 0 && s.[0] = '{' then
s
else "{" ^ s ^ "}"
in
let loc_offset =
(Cmarkit.Textloc.first_byte loc, fst @@ Cmarkit.Textloc.first_line loc)
in
let file = Cmarkit.Textloc.file loc in
let cmarkit =
Cmarkit.Doc.of_string ~loc_offset ~locs:true ~file ~strict:false s
in
let cmarkit = Cmarkit.Doc.block cmarkit in
match cmarkit with
| Cmarkit.Block.Ext_standalone_attributes (attrs, _) -> Ok attrs
| _ -> Error (`Msg "Failed to parse the attributes")
let update_frontmatter (fm : _ fm) v =
{ fm with toplevel_attributes = Some v }
end
module Math_link = struct
type t = string
let key = "math-link"
let of_string (s, _) = Ok s
let update_frontmatter (fm : _ fm) v = { fm with math_link = Some v }
end
module Theme = struct
type t = [ `Builtin of Themes.t | `External of string ]
let key = "theme"
let default = `Builtin Themes.Default
let of_string (s, _) =
match Themes.of_string s with
| Some theme -> Ok (`Builtin theme)
| None -> Ok (`External s)
let update_frontmatter (fm : _ fm) v = { fm with theme = Some v }
end
module Css_links = struct
type t = string list
let key = "css"
let of_string (s, _) =
s |> String.split_on_char ' '
|> List.filter (fun x -> not (String.equal "" x))
|> Result.ok
let update_frontmatter (fm : _ fm) v =
{ fm with css_links = v @ fm.css_links }
end
module Js_links = struct
type t = string list
let key = "js"
let of_string (s, _) =
s |> String.split_on_char ' '
|> List.filter (fun x -> not (String.equal "" x))
|> Result.ok
let update_frontmatter (fm : _ fm) v = { fm with js_links = v @ fm.js_links }
end
module Dimension = struct
type t = int * int
let key = "dimension"
let default = (1440, 1080)
let of_string (s, _) =
let ( let* ) = Result.bind in
let error =
Error
(`Msg "Expected \"4:3\", \"16:9\", or two integers separated by a 'x'")
in
let int_parser i =
match int_of_string_opt i with Some i -> Ok i | None -> error
in
match String.split_on_char 'x' s with
| [ "4:3" ] -> Ok (1440, 1080)
| [ "16:9" ] -> Ok (1920, 1080)
| [ width; height ] ->
let* width = int_parser width in
let* height = int_parser height in
Ok (width, height)
| _ -> error
let update_frontmatter (fm : _ fm) v = { fm with dimension = Some v }
end
module Hljs_theme = struct
type t = string
let key = "highlightjs-theme"
let of_string = fun (x, _) -> Ok x
let default = "default"
let update_frontmatter (fm : _ fm) v = { fm with highlightjs_theme = Some v }
end
module Math_mode = struct
type t = [ `Mathjax | `Katex ]
let key = "math-mode"
let of_string = function
| "mathjax", _ -> Ok `Mathjax
| "katex", _ -> Ok `Katex
| _ -> Error (`Msg "Expected \"mathjax\" or \"katex\"")
let default = `Mathjax
let update_frontmatter (fm : _ fm) v = { fm with math_mode = Some v }
end
module type Field = sig
type t
val key : string
val of_string : string * Cmarkit.Textloc.t -> (t, [ `Msg of string ]) result
val update_frontmatter : string fm -> t -> string fm
end
module External_ids = struct
type t = string list
let key = "external-ids"
let of_string (s, _) =
String.split_on_char ' ' s
|> List.filter (fun x -> not @@ String.equal String.empty x)
|> Result.ok
let update_frontmatter (fm : _ fm) v =
{ fm with external_ids = v @ fm.external_ids }
end
let all_fields =
[
(module Dimension : Field);
(module Toplevel_attributes : Field);
(module Math_link : Field);
(module Theme : Field);
(module Css_links : Field);
(module Js_links : Field);
(module Hljs_theme : Field);
(module Math_mode : Field);
(module External_ids : Field);
]
module SMap = struct
include Map.Make (String)
let of_list bs = List.fold_left (fun m (k, v) -> add k v m) empty bs
end
let fields_map =
all_fields
|> List.map (fun ((module X : Field) as m) -> (X.key, m))
|> SMap.of_list
let fields_names = all_fields |> List.map (fun (module X : Field) -> X.key)
type 'a t =
| Unresolved : string fm -> unresolved t
| Resolved : Asset.t fm -> resolved t
let resolve (Unresolved fm) ~to_asset =
Resolved
{
fm with
math_link = Option.map to_asset fm.math_link;
css_links = List.map to_asset fm.css_links;
js_links = List.map to_asset fm.js_links;
}
let empty_fm =
{
dimension = None;
toplevel_attributes = None;
math_link = None;
theme = None;
css_links = [];
js_links = [];
highlightjs_theme = None;
math_mode = None;
external_ids = [];
}
let empty = Resolved empty_fm
let string_sub s idx idx' = (String.sub s idx idx', (idx, idx + idx' - 1))
let split_in_lines s =
let accumulate n (start_loc : int) i acc =
if start_loc = i then acc else (n, (start_loc, i)) :: acc
in
let rec loop acc start_loc n i =
match s.[i] with
| exception _ -> accumulate n start_loc i acc
| '\r' when i + 1 < String.length s && s.[i + 1] = '\n' ->
loop (accumulate n start_loc i acc) (i + 2) (n + 1) (i + 2)
| '\n' -> loop (accumulate n start_loc i acc) (i + 1) (n + 1) (i + 1)
| _ -> loop acc start_loc n (i + 1)
in
loop [] 0 1 0
|> List.rev_map (fun (n, (x, y)) -> (n, String.sub s x (y - x), (x, y)))
let cut file offset (i, line, (byte_start, _)) c =
let i = i + 1 in
let byte_start = byte_start + offset in
let update_loc (beg, end_) =
Cmarkit.Textloc.v ~file ~first_line:(i, byte_start)
~last_line:(i, byte_start) ~first_byte:(beg + byte_start)
~last_byte:(end_ + byte_start)
in
String.index_opt line c
|> Option.map @@ fun idx ->
let key, kloc = string_sub line 0 idx in
let key = (String.trim key, update_loc kloc) in
let v, loc = string_sub line (idx + 1) (String.length line - (idx + 1)) in
let v = (String.trim v, update_loc loc) in
(key, v)
let send_unrecognized_field ~key ~kloc =
let msg = "Frontmatter field '" ^ key ^ "' is not interpreted by slipshow" in
let n =
"Recognized fields are: '" ^ String.concat "', '" fields_names ^ "'"
in
Diagnosis.add
(General
{ msg; notes = [ n ]; labels = [ ("", kloc) ]; code = "Frontmatter" })
let send_general_error ~key ~msg ~vloc =
Diagnosis.add
(General
{
msg = "Error while parsing frontmatter field '" ^ key ^ "'";
notes = [];
labels = [ (msg, vloc) ];
code = "Frontmatter";
})
let of_string file offset s =
let raise_warning line =
let loc =
let i, _, (byte_start, byte_end) = line in
let i = i + 1 in
let first_byte = byte_start + offset
and last_byte = byte_end + offset - 1 in
Cmarkit.Textloc.v ~file ~first_line:(i, byte_start)
~last_line:(i, byte_start) ~first_byte ~last_byte
in
let msg = "Invalid frontmatter entry" in
let note =
"Frontmatter have to be of the form \"key:value\" on a single line."
in
let notes = [ note ] in
Diagnosis.add
(General { msg; notes; labels = [ ("", loc) ]; code = "Frontmatter" })
in
let assoc =
s |> split_in_lines
|> List.filter_map @@ fun line ->
match cut file offset line ':' with
| None ->
raise_warning line;
None
| Some _ as res -> res
in
let handle_line fm ((key, kloc), ((_, vloc) as value)) =
match SMap.find_opt key fields_map with
| None ->
send_unrecognized_field ~key ~kloc;
fm
| Some (module F) -> (
match F.of_string value with
| Ok x -> F.update_frontmatter fm x
| Error (`Msg msg) ->
send_general_error ~key ~msg ~vloc;
fm)
in
let fm = List.fold_left handle_line empty_fm assoc in
Unresolved fm
let ( let* ) x f = Option.bind x f
let ( let+ ) x f = Option.map f x
let find_opening s =
if
String.starts_with ~prefix:"---\n" s
|| String.starts_with ~prefix:"---\r\n" s
then if s.[4] = '\n' then Some 3 else Some 4
else None
let find_closing s start =
let is_closing idx =
s.[idx + 1] = '-'
&& s.[idx + 2] = '-'
&& s.[idx + 3] = '-'
&& (s.[idx + 4] = '\n' || (s.[idx + 4] = '\r' && s.[idx + 5] = '\n'))
in
let closing_length idx = if s.[idx + 4] = '\n' then 4 else 5 in
let rec aux idx =
match String.index_from_opt s idx '\n' with
| None -> None
| Some idx -> (
try
if is_closing idx then Some (idx + 1, idx + 1 + closing_length idx)
else aux (idx + 1)
with Invalid_argument _ -> None)
in
aux start
let s =
let* start = find_opening s in
let+ end_, after = find_closing s start in
let frontmatter = String.sub s start (end_ - start) in
let rest = String.sub s after (String.length s - after) in
let offset =
let rec n_lines acc index =
if index < 0 then acc
else
let acc = if s.[index] = '\n' then acc + 1 else acc in
n_lines acc (index - 1)
in
(after, n_lines 0 (after - 1))
in
{ frontmatter; rest; rest_offset = offset; fm_offset = start }
let combine (Resolved cli_frontmatter) (Resolved frontmatter) =
let combine_opt cli f = match cli with Some _ as x -> x | None -> f in
let toplevel_attributes =
combine_opt cli_frontmatter.toplevel_attributes
frontmatter.toplevel_attributes
in
let math_link = combine_opt cli_frontmatter.math_link frontmatter.math_link in
let math_mode = combine_opt cli_frontmatter.math_mode frontmatter.math_mode in
let theme = combine_opt cli_frontmatter.theme frontmatter.theme in
let dimension = combine_opt cli_frontmatter.dimension frontmatter.dimension in
let css_links = cli_frontmatter.css_links @ frontmatter.css_links in
let js_links = cli_frontmatter.js_links @ frontmatter.js_links in
let highlightjs_theme =
combine_opt cli_frontmatter.highlightjs_theme frontmatter.highlightjs_theme
in
let external_ids = cli_frontmatter.external_ids @ frontmatter.external_ids in
Resolved
{
toplevel_attributes;
math_link;
theme;
css_links;
dimension;
js_links;
highlightjs_theme;
math_mode;
external_ids;
}