package slipshow

  1. Overview
  2. Docs

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
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;
  dimension : (int * int) option;
}

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;
    }

module Default = struct
  let dimension = (1440, 1080)

  let toplevel_attributes =
    Cmarkit.Attributes.make
      ~kv_attributes:[ (("slip", Cmarkit.Meta.none), None) ]
      ()

  let theme = `Builtin Themes.Default
end

let empty =
  Resolved
    {
      dimension = None;
      toplevel_attributes = None;
      math_link = None;
      theme = None;
      css_links = [];
    }

module String_to = struct
  let toplevel_attributes s =
    let s = String.trim s in
    let s =
      if String.length s > 0 && s.[0] = '{' then
        (* Just so emacs does not find an unmatched curly brace! *)
        let _ = '}' in
        s
      else "{" ^ s ^ "}"
    in
    let cmarkit = Cmarkit.Doc.of_string ~strict:false s in
    let cmarkit = Cmarkit.Doc.block cmarkit in
    match cmarkit with
    | Cmarkit.Block.Ext_standalone_attributes (attrs, _) -> Ok attrs
    | _ -> Error (`Msg "Can only be a set of attributes")

  let math_link s = s

  let theme s =
    match Themes.of_string s with
    | Some theme -> `Builtin theme
    | None -> `External s

  let css_link s = s

  let dimension 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
end

let get (field_name, convert) kv =
  List.assoc_opt field_name kv |> Option.map convert

let cut s c =
  String.index_opt s c
  |> Option.map @@ fun idx ->
     ( String.sub s 0 idx,
       String.trim @@ String.sub s (idx + 1) (String.length s - (idx + 1)) )

let of_string s =
  let assoc =
    s |> String.split_on_char '\n'
    |> List.filter_map @@ fun line ->
       let line = String.trim line in
       cut line ':'
  in
  let get x y =
    match get x y with
    | Some (Ok x) -> Some x
    | Some (Error (`Msg x)) ->
        Logs.warn (fun m -> m "Error in frontmatter: %s" x);
        None
    | None -> None
  in
  let toplevel_attributes =
    get ("toplevel-attributes", String_to.toplevel_attributes) assoc
  in
  let math_link =
    get ("math-link", fun x -> Ok (String_to.math_link x)) assoc
  in
  let theme = get ("theme", fun x -> Ok (String_to.theme x)) assoc in
  let css_links =
    get ("css", fun x -> Ok x) assoc
    |> Option.map (fun x -> String.split_on_char ' ' x)
    |> Option.map @@ List.filter (fun x -> not (String.equal " " x))
    |> Option.value ~default:[]
  in
  let dimension = get ("dimension", String_to.dimension) assoc in
  Ok
    (Unresolved { toplevel_attributes; math_link; theme; css_links; dimension })

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 extract 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
  (frontmatter, rest)

let combine (Resolved cli_frontmatter) (Resolved frontmatter) =
  let combine_opt cli f = match cli with Some _ as x -> x | None -> f in
  (* TODO: warn on cli erasing frontmatter *)
  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 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
  Resolved { toplevel_attributes; math_link; theme; css_links; dimension }
OCaml

Innovation. Community. Security.