Source file splicer.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
open Lang
open Error
open Util.Source

(* Splice key and values *)

module type KEY = sig
  type t

  val to_string : t -> string
  val to_anchor : t -> string
  val parse : Source.t -> t list
  val compare : t -> t -> int
end

module type VALUE = sig
  type t

  val render : t list -> string
end

module type INIT = sig
  type key
  type value

  val init : El.spec -> Pl.spec -> (key * value) list
end

(* Splice lookups *)

module type STORE = sig
  type key
  type value
  type t

  val cardinal : t -> int
  val add : key -> value -> t -> t
  val find_opt : t -> key -> value option
  val use : t -> key -> unit
  val used : t -> key -> bool
  val unused : t -> key list
  val empty : t
  val init : El.spec -> Pl.spec -> t
end

module Make_store
    (K : KEY)
    (V : VALUE)
    (I : INIT with type key = K.t and type value = V.t) :
  STORE with type key = K.t and type value = V.t = struct
  module M = Map.Make (K)

  type key = K.t
  type value = V.t
  type entry = { mutable used : bool; data : V.t }
  type t = entry M.t

  let cardinal (sto : t) : int = M.cardinal sto

  let add (key : K.t) (data : V.t) (sto : t) : t =
    M.add key { used = false; data } sto

  let find_opt (sto : t) (key : K.t) : V.t option =
    match M.find_opt key sto with Some entry -> Some entry.data | None -> None

  let use (sto : t) (key : K.t) : unit =
    let entry = M.find key sto in
    entry.used <- true

  let used (sto : t) (key : K.t) : bool =
    let entry = M.find key sto in
    entry.used

  let unused (sto : t) : K.t list =
    M.fold
      (fun key entry keys_unused ->
        if entry.used then keys_unused else key :: keys_unused)
      sto []
    |> List.rev

  let empty : t = M.empty

  let init (spec_el : El.spec) (spec_pl : Pl.spec) : t =
    I.init spec_el spec_pl
    |> List.fold_left (fun sto (key, data) -> add key data sto) empty
end

(* Splice anchor *)

let prefix_source =
  "ifdef::backend-html5[]\n"
  ^ ".Click to view the specification source\n[%collapsible]\n====\n"
  ^ "[source,watsup]\n----\n"

let suffix_source = "\n----\n====\n\n[.empty]\n--\n\n\n--\n\n" ^ "endif::[]"
let prefix_prose = "****\n"
let suffix_prose = "\n****"

module type ANCHOR = sig
  val name : string
  val prefix : string
  val suffix : string
  val header : bool
end

(* Splicer *)

module type SPLICER = sig
  include ANCHOR

  type key
  type value

  val init : El.spec -> Pl.spec -> unit
  val splice : Source.t -> string
  val warn_unused : unit -> unit
end

module Make
    (K : KEY)
    (V : VALUE)
    (I : INIT with type key = K.t and type value = V.t)
    (A : ANCHOR) : SPLICER with type key = K.t and type value = V.t = struct
  include A

  type key = K.t
  type value = V.t

  (* Store *)

  module S = Make_store (K) (V) (I)

  let sto = ref S.empty

  let init (spec_el : El.spec) (spec_pl : Pl.spec) : unit =
    sto := S.init spec_el spec_pl

  (* Splicer functions *)

  let parse (source : Source.t) : K.t list = K.parse source

  let render (keys : K.t list) : string =
    let keys, values =
      keys
      |> List.filter_map (fun key ->
             let value_opt = S.find_opt !sto key in
             match value_opt with
             | Some value -> Some (key, value)
             | None ->
                 warn no_region
                   (Format.asprintf "%s splice key not found: %s" name
                      (K.to_string key));
                 None)
      |> List.split
    in
    let headers =
      if header then
        (keys
        |> List.filter_map (fun key ->
               if not (S.used !sto key) then Some ("[[" ^ K.to_anchor key ^ "]]")
               else None)
        |> String.concat "\n")
        ^ "\n"
      else ""
    in
    List.iter (S.use !sto) keys;
    headers ^ prefix ^ V.render values ^ suffix

  let splice (source : Source.t) : string =
    let keys = parse source in
    render keys

  let warn_unused () : unit =
    let keys_unused = S.unused !sto in
    let count_unused = List.length keys_unused in
    let total = S.cardinal !sto in
    let percentage =
      if total = 0 then 0.0
      else float_of_int count_unused /. float_of_int total *. 100.0
    in
    Format.asprintf "unused %d %s splices out of %d (%.2f%%)" count_unused name
      total percentage
    |> warn no_region;
    let s =
      keys_unused
      |> List.mapi (fun idx key -> (idx, key))
      |> List.fold_left
           (fun s (idx, key) ->
             let s =
               if idx mod 5 = 0 && idx > 0 then (
                 warn no_region ("\t" ^ s);
                 "")
               else s
             in
             let s = s ^ K.to_string key in
             s ^ if idx mod 5 < 4 && idx < count_unused - 1 then ", " else "")
           ""
    in
    warn no_region ("\t" ^ s)
end