package yocaml

  1. Overview
  2. Docs

Source file archetype.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
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
(* YOCaml a static blog generator.
   Copyright (C) 2024 The Funkyworkers and The YOCaml's developers

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>. *)

let alt_opt a b = match a with Some a -> Some a | None -> b
let is_empty_list = function [] -> true | _ -> false

module Datetime = struct
  type month =
    | Jan
    | Feb
    | Mar
    | Apr
    | May
    | Jun
    | Jul
    | Aug
    | Sep
    | Oct
    | Nov
    | Dec

  type day_of_week = Mon | Tue | Wed | Thu | Fri | Sat | Sun
  type year = int
  type day = int
  type hour = int
  type min = int
  type sec = int

  type t = {
      year : year
    ; month : month
    ; day : day
    ; hour : hour
    ; min : min
    ; sec : sec
  }

  let invalid_int x message =
    Data.Validation.fail_with ~given:(string_of_int x) message

  let month_from_int x =
    if x > 0 && x <= 12 then
      Result.ok
        [| Jan; Feb; Mar; Apr; May; Jun; Jul; Aug; Sep; Oct; Nov; Dec |].(x - 1)
    else invalid_int x "Invalid month value"

  let year_from_int x =
    if x >= 0 then Result.ok x else invalid_int x "Invalid year value"

  let is_leap year =
    if year mod 100 = 0 then year mod 400 = 0 else year mod 4 = 0

  let days_in_month year month =
    match month with
    | Jan | Mar | May | Jul | Aug | Oct | Dec -> 31
    | Feb -> if is_leap year then 29 else 28
    | _ -> 30

  let day_from_int year month x =
    let dim = days_in_month year month in
    if x >= 1 && x <= dim then Result.ok x
    else invalid_int x "Invalid day value"

  let hour_from_int x =
    if x >= 0 && x < 24 then Result.ok x else invalid_int x "Invalid hour value"

  let min_from_int x =
    if x >= 0 && x < 60 then Result.ok x else invalid_int x "Invalid min value"

  let sec_from_int x =
    if x >= 0 && x < 60 then Result.ok x else invalid_int x "Invalid sec value"

  let make_raw ?(time = (0, 0, 0)) ~year ~month ~day () =
    let hour, min, sec = time in
    { year; month; day; hour; min; sec }

  let dummy = make_raw ~time:(0, 0, 0) ~year:1970 ~month:Jan ~day:1 ()
  let ( let* ) = Result.bind

  let make ?(time = (0, 0, 0)) ~year ~month ~day () =
    let hour, min, sec = time in
    let* year = year_from_int year in
    let* month = month_from_int month in
    let* day = day_from_int year month day in
    let* hour = hour_from_int hour in
    let* min = min_from_int min in
    let* sec = sec_from_int sec in
    Result.ok { year; month; day; hour; min; sec }

  let validate_from_datetime_str str =
    let str = String.trim str in
    match
      Scanf.sscanf_opt str "%04d%c%02d%c%02d%c%02d%c%02d%c%02d"
        (fun year _ month _ day _ hour _ min _ sec ->
          ((hour, min, sec), year, month, day))
    with
    | None -> Data.Validation.fail_with ~given:str "Invalid date format"
    | Some (time, year, month, day) -> make ~time ~year ~month ~day ()

  let validate_from_date_str str =
    let str = String.trim str in
    match
      Scanf.sscanf_opt str "%04d%c%02d%c%02d" (fun year _ month _ day ->
          (year, month, day))
    with
    | None -> Data.Validation.fail_with ~given:str "Invalid date format"
    | Some (year, month, day) -> make ~year ~month ~day ()

  let validate =
    let open Data.Validation in
    string & (validate_from_datetime_str / validate_from_date_str)

  let month_to_int = function
    | Jan -> 1
    | Feb -> 2
    | Mar -> 3
    | Apr -> 4
    | May -> 5
    | Jun -> 6
    | Jul -> 7
    | Aug -> 8
    | Sep -> 9
    | Oct -> 10
    | Nov -> 11
    | Dec -> 12

  let month_to_string = function
    | Jan -> "jan"
    | Feb -> "feb"
    | Mar -> "mar"
    | Apr -> "apr"
    | May -> "may"
    | Jun -> "jun"
    | Jul -> "jul"
    | Aug -> "aug"
    | Sep -> "sep"
    | Oct -> "oct"
    | Nov -> "nov"
    | Dec -> "dec"

  let dow_to_int = function
    | Mon -> 0
    | Tue -> 1
    | Wed -> 2
    | Thu -> 3
    | Fri -> 4
    | Sat -> 5
    | Sun -> 6

  let dow_to_string = function
    | Mon -> "mon"
    | Tue -> "tue"
    | Wed -> "wed"
    | Thu -> "thu"
    | Fri -> "fri"
    | Sat -> "sat"
    | Sun -> "sun"

  let compare_date a b =
    let cmp = Int.compare a.year b.year in
    if Int.equal cmp 0 then
      let cmp = Int.compare (month_to_int a.month) (month_to_int b.month) in
      if Int.equal cmp 0 then Int.compare a.day b.day else cmp
    else cmp

  let compare_time a b =
    let cmp = Int.compare a.hour b.hour in
    if Int.equal cmp 0 then
      let cmp = Int.compare a.min b.min in
      if Int.equal cmp 0 then Int.compare a.sec b.sec else cmp
    else cmp

  let compare a b =
    let cmp = compare_date a b in
    if Int.equal cmp 0 then compare_time a b else cmp

  let equal a b = Int.equal 0 (compare a b)

  let pp_date ppf { year; month; day; _ } =
    Format.fprintf ppf "%04d-%02d-%02d" year (month_to_int month) day

  let pp_time ppf { hour; min; sec; _ } =
    Format.fprintf ppf "%02d:%02d:%02d" hour min sec

  let pp ppf dt = Format.fprintf ppf "%a %a" pp_date dt pp_time dt

  let month_value = function
    | Jan -> 0
    | Feb -> 3
    | Mar -> 3
    | Apr -> 6
    | May -> 1
    | Jun -> 4
    | Jul -> 6
    | Aug -> 2
    | Sep -> 5
    | Oct -> 0
    | Nov -> 3
    | Dec -> 5

  let day_of_week { year; month; day; _ } =
    let yy = year mod 100 in
    let cc = (year - yy) / 100 in
    let c_code = [| 6; 4; 2; 0 |].(cc mod 4) in
    let y_code = (yy + (yy / 4)) mod 7 in
    let m_code =
      let v = month_value month in
      if is_leap year && (month = Jan || month = Feb) then v - 1 else v
    in
    let index = (c_code + y_code + m_code + day) mod 7 in
    [| Sun; Mon; Tue; Wed; Thu; Fri; Sat |].(index)

  let pp_rfc822 ?(tz = "gmt") () ppf dt =
    let tz = String.uppercase_ascii tz in
    let dow = dt |> day_of_week |> dow_to_string |> String.capitalize_ascii in
    let mon = dt.month |> month_to_string |> String.capitalize_ascii in
    Format.fprintf ppf "%s, %02d %s %04d %a %s" dow dt.day mon dt.year pp_time
      dt tz

  let pp_rfc3339 ?(tz = "Z") () ppf dt =
    let mon = dt.month |> month_to_int in
    Format.fprintf ppf "%04d-%02d-%02dT%02d:%02d:%02d%s" dt.year mon dt.day
      dt.hour dt.min dt.sec tz

  let normalize ({ year; month; day; hour; min; sec } as dt) =
    let has_time = not (Int.equal (compare_time dt dummy) 0) in
    let datetime_repr = Format.asprintf "%a" pp dt in
    let date_repr = Format.asprintf "%a" pp_date dt in
    let time_repr = Format.asprintf "%a" pp_time dt in
    let day_of_week = day_of_week dt in
    let open Data in
    record
      [
        ("year", int year)
      ; ("month", int (month_to_int month))
      ; ("day", int day)
      ; ("hour", int hour)
      ; ("min", int min)
      ; ("sec", int sec)
      ; ("has_time", bool has_time)
      ; ("day_of_week", int (dow_to_int day_of_week))
      ; ( "repr"
        , record
            [
              ("month", string (month_to_string month))
            ; ("datetime", string datetime_repr)
            ; ("date", string date_repr)
            ; ("time", string time_repr)
            ; ("day_of_week", string (dow_to_string day_of_week))
            ] )
      ]

  module Infix = struct
    let ( = ) = equal
    let ( <> ) x y = not (equal x y)
    let ( > ) x y = compare x y > 0
    let ( >= ) x y = compare x y >= 0
    let ( < ) x y = compare x y < 0
    let ( <= ) x y = compare x y <= 0
  end

  let min a b = if Infix.(b > a) then a else b
  let max a b = if Infix.(a < b) then a else b

  include Infix
end

module Page = struct
  let entity_name = "Page"

  class type t = object ('a)
    method page_title : string option
    method page_charset : string option
    method description : string option
    method tags : string list
    method display_toc : bool
    method toc : string option
    method with_toc : string option -> 'a
  end

  class page ?title ?description ?charset ?(tags = []) ?(display_toc = false)
    ?toc () =
    object (_ : #t)
      val toc_value = toc
      method page_title = title
      method page_charset = charset
      method description = description
      method tags = tags
      method display_toc = display_toc
      method toc = toc_value
      method with_toc toc_value = {<toc_value>}
    end

  let title p = p#page_title
  let charset p = p#page_charset
  let description p = p#description
  let tags p = p#tags
  let neutral = Result.ok @@ new page ()

  let validate_page fields =
    let open Data.Validation in
    let+ title = optional fields "page_title" string
    and+ description = optional fields "description" string
    and+ charset = optional fields "page_charset" string
    and+ tags = optional_or fields ~default:[] "tags" (list_of string)
    and+ display_toc = optional fields "display_toc" bool in
    new page ?title ?description ?charset ~tags ?display_toc ()

  let with_toc m toc = m#with_toc toc

  let validate =
    let open Data.Validation in
    record validate_page

  let to_meta name = function
    | None -> []
    | Some x ->
        [ Data.(record [ ("name", string name); ("content", string x) ]) ]

  let to_meta_kwd = function
    | [] -> []
    | tags ->
        [
          Data.(
            record
              [
                ("name", string "keywords")
              ; ("content", string @@ String.concat ", " tags)
              ])
        ]

  let meta_list p = to_meta "description" p#description @ to_meta_kwd p#tags
  let has_toc obj = obj#display_toc && Option.is_some obj#toc

  let normalize_parameters obj =
    Data.
      [
        ("page_title", option string obj#page_title)
      ; ("description", option string obj#description)
      ; ("page_charset", option string obj#page_charset)
      ; ("tags", list_of string obj#tags)
      ; ("has_tags", bool (not (is_empty_list obj#tags)))
      ; ("has_page_title", bool @@ Option.is_some obj#page_title)
      ; ("has_description", bool @@ Option.is_some obj#description)
      ; ("has_page_charset", bool @@ Option.is_some obj#page_charset)
      ; ("has_toc", bool @@ has_toc obj)
      ; ("toc", option string obj#toc)
      ]

  let normalize_meta obj = Data.[ ("meta", list @@ meta_list obj) ]
  let normalize obj = normalize_parameters obj @ normalize_meta obj
end

module Article = struct
  let entity_name = "Article"

  class type t = object
    inherit Page.t
    method title : string
    method synopsis : string option
    method date : Datetime.t
  end

  class article page ?synopsis ~title ~date () =
    let page_title = alt_opt page#page_title (Some title) in
    let description = alt_opt page#description synopsis in
    object (_ : #t)
      inherit
        Page.page
          ?title:page_title ?description ?charset:page#page_charset
            ~tags:page#tags ~display_toc:page#display_toc ?toc:page#toc ()

      method title = title
      method synopsis = synopsis
      method date = date
    end

  let page a = (a :> Page.t)
  let title a = a#title
  let synopsis a = a#synopsis
  let date a = a#date
  let with_toc a = a#with_toc

  let neutral =
    Data.Validation.fail_with ~given:"null" "Cannot be null"
    |> Result.map_error (fun error ->
           Required.Validation_error { entity = entity_name; error })

  let validate =
    let open Data.Validation in
    record (fun fields ->
        let+ page = Page.validate_page fields
        and+ title = required fields "title" string
        and+ synopsis = optional fields "synopsis" string
        and+ date = required fields "date" Datetime.validate in
        new article page ?synopsis ~title ~date ())

  let normalize obj =
    Page.normalize obj
    @ Data.
        [
          ("title", string obj#title)
        ; ("synopsis", option string obj#synopsis)
        ; ("date", Datetime.normalize obj#date)
        ; ("has_synopsis", bool @@ Option.is_some obj#synopsis)
        ]
end

module Articles = struct
  class type t = object
    inherit Page.t
    method articles : (Path.t * Article.t) list
  end

  class articles page articles =
    object (_ : #t)
      inherit
        Page.page
          ?title:page#page_title ?description:page#description
            ?charset:page#page_charset ~tags:page#tags ?toc:page#toc
            ~display_toc:page#display_toc ()

      method articles = articles
    end

  let with_toc a = a#with_toc
  let from_page = Task.lift (fun (page, articles) -> new articles page articles)

  let sort_by_date ?(increasing = false) articles =
    List.sort
      (fun (_, articleA) (_, articleB) ->
        let r = Datetime.compare articleA#date articleB#date in
        if increasing then r else ~-r)
      articles

  let fetch (module P : Required.DATA_PROVIDER) ?increasing
      ?(filter = fun x -> x) ?(on = `Source) ~where ~compute_link path =
    Task.from_effect (fun () ->
        let open Eff in
        let* files = read_directory ~on ~only:`Files ~where path in
        let+ articles =
          List.traverse
            (fun file ->
              let url = compute_link file in
              let+ metadata, _content =
                Eff.read_file_with_metadata (module P) (module Article) ~on file
              in
              (url, metadata))
            files
        in
        articles |> sort_by_date ?increasing |> filter)

  let compute_index (module P : Required.DATA_PROVIDER) ?increasing
      ?(filter = fun x -> x) ?(on = `Source) ~where ~compute_link path =
    let open Task in
    (fun x -> (x, ()))
    |>> second
          (fetch (module P) ?increasing ~filter ~on ~where ~compute_link path)
    >>> from_page

  let normalize_article (ident, article) =
    let open Data in
    record (("url", string @@ Path.to_string ident) :: Article.normalize article)

  let normalize obj =
    let open Data in
    ("articles", list_of normalize_article obj#articles)
    :: ("has_articles", bool @@ is_empty_list obj#articles)
    :: Page.normalize obj
end
OCaml

Innovation. Community. Security.