package forester

  1. Overview
  2. Docs

Source file Forest.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
open Eio.Std
open Prelude
open Core
open Render

module T = Domainslib.Task

module Addr = String
module Tbl = Hashtbl.Make (Addr)
module Gph = Graph.Imperative.Digraph.Concrete (Addr)
module Topo = Graph.Topological.Make (Gph)
module Clo = Graph.Traverse

module M = Map.Make (String)

module type S =
sig
  val plant_tree : sourcePath:string option -> addr -> Code.doc -> unit
  val create_tree : dir:string -> prefix:string -> addr
  val complete : string -> (addr * string) Seq.t
  val render_trees : unit -> unit
end

module type I =
sig
  val env : Eio_unix.Stdenv.base
  val root : addr option
  val base_url : string option
  val ignore_tex_cache : bool
  val max_fibers : int
end

module Make (I : I) : S =
struct
  module LaTeXQueue = LaTeXQueue.Make (I)
  let size = 100

  let frozen = ref false
  let unexpanded_trees : Code.doc Tbl.t = Tbl.create size

  let sourcePaths : string Tbl.t = Tbl.create size
  let import_graph : Gph.t = Gph.create ()

  let transclusion_graph : Gph.t = Gph.create ()
  let link_graph : Gph.t = Gph.create ()
  let tag_graph : Gph.t = Gph.create ()
  let author_pages : addr Tbl.t = Tbl.create 10
  let contributors : addr Tbl.t = Tbl.create size
  let bibliography : addr Tbl.t = Tbl.create size

  let run_renderer (docs : Sem.doc M.t) (body : unit -> 'a) : 'a =
    let module S = Set.Make (String) in
    let module H : RenderEff.Handler =
    struct
      let is_root addr =
        I.root = Some addr

      let route addr =
        match is_root addr with
        | true -> "index.xml"
        | false -> addr ^ ".xml"

      let abs_path addr =
        Tbl.find_opt sourcePaths addr

      let get_doc addr =
        M.find_opt addr docs

      let enqueue_latex ~name ~packages ~source =
        LaTeXQueue.enqueue ~name ~packages ~source

      let addr_peek_title scope =
        match M.find_opt scope docs with
        | Some doc -> Sem.Doc.peek_title doc
        | None -> None

      let get_sorted_trees addrs : Sem.doc list =
        let find addr =
          match M.find_opt addr docs with
          | None -> []
          | Some doc -> [doc]
        in
        Sem.Doc.sort @@ List.concat_map find @@ S.elements addrs

      let get_all_links scope =
        get_sorted_trees @@ S.of_list @@ Gph.pred link_graph scope

      let backlinks scope =
        get_sorted_trees @@ S.of_list @@ Gph.succ link_graph scope

      let related scope =
        get_all_links scope |> List.filter @@ fun (doc : Sem.doc) ->
        not (doc.taxon = Some "reference")

      let bibliography scope =
        get_sorted_trees @@
        S.of_list @@ Tbl.find_all bibliography scope

      let parents scope =
        get_sorted_trees @@ S.of_list @@ Gph.succ transclusion_graph scope

      let contributions scope =
        get_sorted_trees @@ S.of_list @@ Tbl.find_all author_pages scope

      let contributors scope =
        let doc = M.find scope docs in
        let authors = S.of_list doc.authors in
        let contributors = S.of_list @@ Tbl.find_all contributors scope in
        let proper_contributors =
          contributors |> S.filter @@ fun contr ->
          not @@ S.mem contr authors
        in
        let by_title = Compare.under addr_peek_title @@ Compare.option String.compare in
        let compare = Compare.cascade by_title String.compare in
        List.sort compare @@ S.elements proper_contributors

      let rec test_query query (doc : Sem.doc) =
        match query with
        | Query.Author [Sem.Text addr] ->
          List.mem addr doc.authors
        | Query.Tag [Sem.Text addr] ->
          List.mem addr doc.tags
        | Query.Meta (key, value) ->
          List.mem (key, value) doc.metas
        | Query.Taxon [Sem.Text taxon] ->
          doc.taxon = Some taxon
        | Query.Or qs ->
          qs |> List.exists @@ fun q -> test_query q doc
        | Query.And qs ->
          qs |> List.for_all @@ fun q -> test_query q doc
        | Query.Not q ->
          not @@ test_query q doc
        | Query.True ->
          true
        | _ -> false

      let run_query query =
        get_sorted_trees @@ S.of_seq @@ Seq.map fst @@ M.to_seq @@
        M.filter (fun _ doc -> test_query query doc) docs
    end
    in
    let module Run = RenderEff.Run (H) in
    Run.run body

  let expand_transitive_contributors_and_bibliography (trees : Sem.doc M.t) : unit =
    begin
      trees |> M.iter @@ fun addr _ ->
      let task ref =
        match M.find_opt ref trees with
        | None -> ()
        | Some (doc : Sem.doc) ->
          if doc.taxon = Some "reference" then
            Tbl.add bibliography addr ref
      in
      Gph.iter_pred task link_graph addr
    end;
    transclusion_graph |> Topo.iter @@ fun addr ->
    let task addr' =
      let doc = M.find addr trees in
      begin
        doc.authors @ Tbl.find_all contributors addr |> List.iter @@ fun contributor ->
        Tbl.add contributors addr' contributor
      end;
      begin
        Tbl.find_all bibliography addr |> List.iter @@ fun ref ->
        Tbl.add bibliography addr' ref
      end
    in
    Gph.iter_succ task transclusion_graph addr

  let rec analyze_nodes scope : Sem.t -> unit =
    List.iter @@
    function
    | Sem.Text _ -> ()
    | Sem.Transclude (opts, addr) ->
      analyze_transclusion_opts scope opts;
      Gph.add_edge transclusion_graph addr scope
    | Sem.Link {title; dest} ->
      analyze_nodes scope title;
      Gph.add_edge link_graph dest scope
    | Sem.Tag (_, _, xs) ->
      analyze_nodes scope xs
    | Sem.Math (_, x) ->
      analyze_nodes scope x
    | Sem.EmbedTeX {source; _} ->
      analyze_nodes scope source
    | Sem.Block (title, body) ->
      analyze_nodes scope title;
      analyze_nodes scope body
    | Sem.Query (opts, _) ->
      analyze_transclusion_opts scope opts

  and analyze_transclusion_opts scope : Sem.transclusion_opts -> unit =
    function Sem.{title_override; _} ->
      title_override |> Option.iter @@ analyze_nodes scope

  let rec process_decl scope =
    function
    | Code.Tag tag ->
      Gph.add_edge tag_graph tag scope
    | Code.Author author ->
      Tbl.add author_pages author scope
    | Code.Import (_, dep) ->
      Gph.add_edge import_graph dep scope
    | _ -> ()

  and process_decls scope =
    List.iter @@ process_decl scope


  let plant_tree ~(sourcePath : string option) scope (doc : Code.doc) : unit =
    assert (not !frozen);
    if Tbl.mem unexpanded_trees scope then
      failwith @@ Format.asprintf "Duplicate tree %s" scope;
    sourcePath |> Option.iter @@ Tbl.add sourcePaths scope;
    Gph.add_vertex transclusion_graph scope;
    Gph.add_vertex link_graph scope;
    Gph.add_vertex import_graph scope;
    Gph.add_vertex tag_graph scope;
    process_decls scope doc;
    Tbl.add unexpanded_trees scope doc


  let prepare_forest ()  =
    frozen := true;

    let docs =
      begin
        let task addr (units, trees) =
          let doc = Tbl.find unexpanded_trees addr in
          let units, doc = Expand.expand_doc units addr doc in
          let doc = Eval.eval_doc doc in
          units, M.add addr doc trees
        in
        snd @@ Topo.fold task import_graph (Expand.UnitMap.empty, M.empty)
      end
    in

    begin
      docs |> M.iter @@ fun scope Sem.{body; title; metas; _} ->
      analyze_nodes scope body;
      title |> Option.iter @@ analyze_nodes scope;
      metas |> List.iter @@ fun (_, meta) ->
      analyze_nodes scope meta
    end;

    expand_transitive_contributors_and_bibliography docs;
    docs

  let next_addr ~prefix docs =
    let keys =
      M.to_seq docs |> Seq.map fst |> Seq.filter_map @@ fun addr ->
      match String.split_on_char '-' addr with
      | [prefix'; str] when prefix' = prefix ->
        BaseN.Base36.int_of_string str
      | _ -> None
    in
    let next = 1 + Seq.fold_left max 0 keys in
    prefix ^ "-" ^ BaseN.Base36.string_of_int next

  let create_tree ~dir ~prefix =
    let docs = prepare_forest () in
    let next = next_addr docs ~prefix in
    let fname = next ^ ".tree" in
    let now = Date.now () in
    let body = Format.asprintf "\\date{%a}\n" Date.pp now in
    let create = `Exclusive 0o644 in
    let path = Eio.Path.(Eio.Stdenv.cwd I.env / dir / fname) in
    Eio.Path.save ~create path body;
    next

  let complete prefix =
    prepare_forest ()
    |> M.filter_map (fun _ -> Sem.Doc.peek_title)
    |> M.filter (fun _ -> String.starts_with ~prefix)
    |> M.to_seq

  module E = RenderEff.Perform

  let render_doc ~cwd ~docs ~bib_fmt doc =
    RenderBibTeX.render_bibtex ~base_url:I.base_url doc bib_fmt;
    Format.fprintf bib_fmt "\n";

    doc.addr |> Option.iter @@ fun addr ->
    let create = `Or_truncate 0o644 in
    begin
      let path = Eio.Path.(cwd / "output" / E.route addr) in
      Eio.Path.with_open_out ~create path @@ fun flow ->
      Eio.Buf_write.with_flow flow @@ fun w ->
      let out = Xmlm.make_output @@ Eio_util.xmlm_dest_of_writer w in
      RenderXml.render_doc_page ~trail:(Some Emp) doc out
    end;
    begin
      let path = Eio.Path.(cwd / "latex" / (addr ^ ".tex")) in
      Eio.Path.with_open_out ~create path @@ fun flow ->
      Eio.Buf_write.with_flow flow @@ fun w ->
      RenderLaTeX.render_doc_page ~base_url:I.base_url doc @@ Eio_util.formatter_of_writer w
    end

  let render_json ~cwd docs =
    let create = `Or_truncate 0o644 in
    let json_path = Eio.Path.(cwd / "output" / "forest.json") in
    Eio.Path.with_open_out ~create json_path @@ fun json_sink ->
    Eio.Buf_write.with_flow json_sink @@ fun w ->
    let fmt = Eio_util.formatter_of_writer w in
    let docs = Sem.Doc.sort @@ List.of_seq @@ Seq.map snd @@ M.to_seq docs in
    RenderJson.render_docs docs fmt

  let copy_assets ~env =
    let cwd = Eio.Stdenv.cwd env in
    Eio.Path.with_open_dir Eio.Path.(cwd / "assets") @@ fun assets ->
    Eio.Path.read_dir assets |> List.iter @@ fun fname ->
    let source = "assets/" ^ fname in
    Eio_util.copy_to_dir ~env ~cwd ~source ~dest_dir:"build";
    Eio_util.copy_to_dir ~env ~cwd ~source ~dest_dir:"output";
    Eio_util.copy_to_dir ~env ~cwd ~source ~dest_dir:"latex"

  let copy_resources ~env =
    let cwd = Eio.Stdenv.cwd env in
    Eio.Path.with_open_dir Eio.Path.(cwd / "build") @@ fun build ->
    Eio.Path.read_dir build |> List.iter @@ fun fname ->
    let ext = Filename.extension fname in
    let fp = Format.sprintf "build/%s" fname in
    begin
      match ext with
      | ".svg" -> Some "output/resources";
      | ".pdf" -> Some "latex/resources"
      | _ -> None
    end |> Option.iter @@ fun dest_dir ->
    Eio_util.copy_to_dir ~cwd ~env ~source:fp ~dest_dir

  let with_bib_fmt ~cwd kont =
    let create = `Or_truncate 0o644 in
    let bib_path = Eio.Path.(cwd / "latex" / "forest.bib") in
    Eio.Path.with_open_out ~append:true ~create bib_path @@ fun bib_sink ->
    Eio.Buf_write.with_flow bib_sink @@ fun bib_w ->
    kont @@ Eio_util.formatter_of_writer bib_w

  let render_trees () : unit =
    let docs = prepare_forest () in

    let env = I.env in
    let cwd = Eio.Stdenv.cwd env in

    Eio_util.ensure_dir @@ Eio.Path.(cwd / "build");
    Eio_util.ensure_dir_path cwd ["output"; "resources"];
    Eio_util.ensure_dir_path cwd ["latex"; "resources"];

    run_renderer docs @@ fun () ->
    with_bib_fmt ~cwd @@ fun bib_fmt ->
    docs |> M.iter (fun _ -> render_doc ~cwd ~docs ~bib_fmt);
    render_json ~cwd docs;
    copy_assets ~env;
    LaTeXQueue.process ~env;
    copy_resources ~env
end
OCaml

Innovation. Community. Security.