From eb6f852a99dfbca7379446cc710d9076fd42a575 Mon Sep 17 00:00:00 2001
From: Virgile Prevosto <virgile.prevosto@m4x.org>
Date: Tue, 22 Oct 2019 13:22:33 +0200
Subject: [PATCH] [library] Promote MdR markdown module as the main Frama-C
 markdown lib

---
 src/libraries/utils/markdown.ml               |  68 ++++++++++--
 src/libraries/utils/markdown.mli              |  54 ++++++++--
 src/plugins/markdown-report/Makefile          |   2 +-
 .../markdown-report/Report_markdown.mli       | 100 ------------------
 src/plugins/markdown-report/md_gen.ml         |  16 +--
 src/plugins/markdown-report/sarif.ml          |  10 +-
 src/plugins/markdown-report/sarif_gen.ml      |   2 +-
 src/plugins/server/data.ml                    |  26 +++--
 src/plugins/server/doc.ml                     |  83 +++++++++------
 src/plugins/server/doc.mli                    |   6 +-
 src/plugins/server/kernel_ast.ml              |  10 +-
 src/plugins/server/kernel_main.ml             |  35 +++---
 src/plugins/server/kernel_project.ml          |  16 +--
 src/plugins/server/request.ml                 |  39 ++++---
 src/plugins/server/syntax.ml                  |  94 ++++++++--------
 src/plugins/server/syntax.mli                 |   4 +-
 16 files changed, 288 insertions(+), 277 deletions(-)
 delete mode 100644 src/plugins/markdown-report/Report_markdown.mli

diff --git a/src/libraries/utils/markdown.ml b/src/libraries/utils/markdown.ml
index 7ba2e768fe1..e1fec7d81ef 100644
--- a/src/libraries/utils/markdown.ml
+++ b/src/libraries/utils/markdown.ml
@@ -30,8 +30,8 @@ and block = block_element list
 and element =
   | Block of block
   | Raw of string list
-   (** non-markdown. Each element of the list is printed as-is on its own line.
-       A blank line separates the [Raw] node from the next one. *)
+  (** non-markdown. Each element of the list is printed as-is on its own line.
+      A blank line separates the [Raw] node from the next one. *)
   | Comment of string (** markdown comment, printed <!-- like this --> *)
   | H1 of text * string option (** optional label. *)
   | H2 of text * string option
@@ -40,19 +40,23 @@ and element =
   | H5 of text * string option
   | H6 of text * string option
   | Table of { caption: text option; header: (text * align) list;
-                content: text list list; }
+               content: text list list; }
+
+type elements = element list
 
 type pandoc_markdown =
   { title: text;
     authors: text list;
     date: text;
-    elements: element list
+    elements: elements
   }
 
 let plain s = [ Plain s]
 
 let plain_format txt = Format.kasprintf plain txt
 
+let link_current_page sec = Section("", sec)
+
 let plain_link h =
   let s = match h with
     | URL url -> url
@@ -67,6 +71,26 @@ let codelines lang pp code =
   let lines = String.split_on_char '\n' s in
   Code_block (lang, lines)
 
+let raw_markdown filename =
+  let chan = open_in filename in
+  let res = ref [] in
+  try
+    while true do
+      res := input_line chan :: !res;
+    done;
+    assert false
+  with End_of_file ->
+    close_in chan;
+    Raw (List.rev !res)
+
+let glue ?(sep=[]) texts =
+  let rec aux = function
+    | [] -> []
+    | [t] -> t
+    | hd::tl -> hd @ sep @ aux tl
+  in
+  aux texts
+
 let id m =
   let buffer = Buffer.create (String.length m) in
   let lowercase = Char.lowercase_ascii in
@@ -84,6 +108,36 @@ let id m =
       | _ -> ()) m ;
   Buffer.contents buffer
 
+let section ?name ~title elements =
+  let anchor =
+    match name with
+    | None -> id title
+    | Some n -> n
+  in
+  (H1 (plain title, Some anchor)) :: elements
+
+let subsections header body =
+  let body =
+    List.map
+      (function
+        | H1(t,h) -> H2(t,h)
+        | H2(t,h) -> H3(t,h)
+        | H3(t,h) -> H4(t,h)
+        | H4(t,h) -> H5(t,h)
+        | e -> e)
+      (List.concat body)
+  in
+  header @ body
+
+let mk_date () =
+  let tm = Unix.gmtime (Unix.time()) in
+  plain
+    (Printf.sprintf "%d-%02d-%02d"
+       (1900 + tm.Unix.tm_year) (1 + tm.Unix.tm_mon) tm.Unix.tm_mday)
+
+let pandoc ?(title=plain "") ?(authors=[]) ?(date=mk_date()) elements =
+  { title; authors; date; elements }
+
 let pp_href fmt = function
   | URL s | Page s -> Format.pp_print_string fmt s
   | Section (p,s) -> Format.fprintf fmt "%s#%s" p (id s)
@@ -116,9 +170,9 @@ let pp_dashes fmt size =
   Format.fprintf fmt "%s+" dashes
 
 let pp_sep_line fmt sizes =
-Format.fprintf fmt "@[<h>+";
-List.iter (pp_dashes fmt) sizes;
-Format.fprintf fmt "@]@\n"
+  Format.fprintf fmt "@[<h>+";
+  List.iter (pp_dashes fmt) sizes;
+  Format.fprintf fmt "@]@\n"
 
 let pp_header fmt (t,_) size =
   let real_size = test_size t in
diff --git a/src/libraries/utils/markdown.mli b/src/libraries/utils/markdown.mli
index 5580883687b..2cdef522113 100644
--- a/src/libraries/utils/markdown.mli
+++ b/src/libraries/utils/markdown.mli
@@ -30,8 +30,8 @@ and block = block_element list
 and element =
   | Block of block
   | Raw of string list
-   (** non-markdown. Each element of the list is printed as-is on its own line.
-       A blank line separates the [Raw] node from the next one. *)
+  (** non-markdown. Each element of the list is printed as-is on its own line.
+      A blank line separates the [Raw] node from the next one. *)
   | Comment of string (** markdown comment, printed <!-- like this --> *)
   | H1 of text * string option (** optional label. *)
   | H2 of text * string option
@@ -40,24 +40,64 @@ and element =
   | H5 of text * string option
   | H6 of text * string option
   | Table of { caption: text option; header: (text * align) list;
-                content: text list list; }
+               content: text list list; }
+
+type elements = element list
 
 type pandoc_markdown =
   { title: text;
     authors: text list;
     date: text;
-    elements: element list
+    elements: elements
   }
 
+(** creates a document from a list of elements and optional metadatas.
+    Defaults are:
+    - title: empty
+    - authors: empty list
+    - date: current day, in ISO format
+*)
+val pandoc:
+  ?title:text -> ?authors: text list -> ?date: text -> elements ->
+  pandoc_markdown
+
+(** get the content of a file as raw markdown.
+    @raise Sys_error if there's no such file.
+*)
+val raw_markdown: string -> element
+
 val plain: string -> text
 
 val plain_format: ('a, Format.formatter, unit, text) format4 -> 'a
 
+(** glue text fragments. *)
+val glue: ?sep: text -> text list -> text
+
+(** transforms a string into an anchor name, roughly following
+    pandoc's conventions.
+*)
+val id: string -> string
+
+(** adds a [H1] header with the given [title] on top of the given elements.
+    If name is not explicitly provided,
+    the header will have as associated anchor [id title]
+*)
+val section: ?name:string -> title:string -> elements -> elements
+
+(** [subsections header body] returns a list of [element]s where the [body]'s
+    headers have been increased by one (i.e. [H1] becomes [H2]).
+    [H5] stays at [H5], though.
+*)
+val subsections: elements -> elements list -> elements
+
+(** returns an internal link relative to the current page *)
+val link_current_page: string -> href
+
 (** gives a link whose text is the URL itself. *)
-val plain_link: string -> inline
+val plain_link: href -> inline
 
 (** [codelines lang pp code] returns a [Code_block] for [code], written
-in [lang], as pretty-printed by [pp]. *)
+    in [lang], as pretty-printed by [pp]. *)
 val codelines:
   string -> (Format.formatter -> 'a -> unit) -> 'a -> block_element
 
@@ -71,6 +111,6 @@ val pp_block: Format.formatter -> block -> unit
 
 val pp_element: Format.formatter -> element -> unit
 
-val pp_elements: Format.formatter -> element list -> unit
+val pp_elements: Format.formatter -> elements -> unit
 
 val pp_pandoc: Format.formatter -> pandoc_markdown -> unit
diff --git a/src/plugins/markdown-report/Makefile b/src/plugins/markdown-report/Makefile
index d9b339efd9a..12c2879b68e 100644
--- a/src/plugins/markdown-report/Makefile
+++ b/src/plugins/markdown-report/Makefile
@@ -7,7 +7,7 @@ Report_markdown_VERSION:=0.1~beta
 PLUGIN_NAME:=Report_markdown
 PLUGIN_GENERATED:=$(PLUGIN_DIR)/mdr_version.ml
 PLUGIN_CMO:=\
-  markdown sarif mdr_version mdr_params parse_remarks \
+  sarif mdr_version mdr_params parse_remarks \
   eva_coverage md_gen sarif_gen mdr_register
 PLUGIN_NO_TEST:=true
 PLUGIN_REQUIRES:=ppx_deriving ppx_deriving_yojson yojson
diff --git a/src/plugins/markdown-report/Report_markdown.mli b/src/plugins/markdown-report/Report_markdown.mli
deleted file mode 100644
index 9ba082ead85..00000000000
--- a/src/plugins/markdown-report/Report_markdown.mli
+++ /dev/null
@@ -1,100 +0,0 @@
-module Mdr_params: sig
-include Plugin.S
-
-(** Value of [-mdr-out]. *)
-module Output: Parameter_sig.String
-
-(** Value of [-mdr-gen]. *)
-module Generate: Parameter_sig.String
-
-(** Value of [-mdr-remarks]. *)
-module Remarks: Parameter_sig.String
-
-(** Value of [-mdr-flamegraph]. *)
-module FlameGraph: Parameter_sig.String
-
-(** Value of [-mdr-authors]. *)
-module Authors: Parameter_sig.String_list
-
-(** Value of [-mdr-title]. *)
-module Title: Parameter_sig.String
-
-(** Value of [-mdr-stubs]. *)
-module Stubs: Parameter_sig.String_list
-end
-module Markdown: sig
-type align = Left | Center | Right
-
-type inline =
-  | Plain of string
-  | Emph of string
-  | Bold of string
-  | Inline_code of string
-  | Link of text * string (** [Link(text,url)] *)
-  | Image of string * string (** [Image(alt,location)] *)
-
-and text = inline list
-
-type block_element =
-  | Text of text (** single paragraph of text. *)
-  | Block_quote of element list
-  | UL of block list
-  | OL of block list
-  | DL of (text * text) list (** definition list *)
-  | EL of (string option * text) list (** example list *)
-  | Code_block of string * string list
-
-and block = block_element list
-
-and element =
-  | Block of block
-  | Raw of string list
-   (** non-markdown. Each element of the list is printed as-is on its own line.
-       A blank line separates the [Raw] node from the next one. *)
-  | Comment of string (** markdown comment, printed <!-- like this --> *)
-  | H1 of text * string option (** optional label. *)
-  | H2 of text * string option
-  | H3 of text * string option
-  | H4 of text * string option
-  | H5 of text * string option
-  | H6 of text * string option
-  | Table of { caption: text option; header: (text * align) list;
-                content: text list list; }
-
-type pandoc_markdown =
-  { title: text;
-    authors: text list;
-    date: text;
-    elements: element list
-  }
-
-val plain: string -> text
-
-val plain_format: ('a, Format.formatter, unit, text) format4 -> 'a
-
-(** gives a link whose text is the URL itself. *)
-val plain_link: string -> inline
-
-(** [codelines lang pp code] returns a [Code_block] for [code], written
-in [lang], as pretty-printed by [pp]. *)
-val codelines:
-  string -> (Format.formatter -> 'a -> unit) -> 'a -> block_element
-
-val pp_inline: Format.formatter -> inline -> unit
-
-val pp_text: Format.formatter -> text -> unit
-
-val pp_block_element: Format.formatter -> block_element -> unit
-
-val pp_block: Format.formatter -> block -> unit
-
-val pp_element: Format.formatter -> element -> unit
-
-val pp_elements: Format.formatter -> element list -> unit
-
-val pp_pandoc: Format.formatter -> pandoc_markdown -> unit
-end
-module Md_gen: sig
-(** generates the report (either final or [draft] according to the flag) *)
-val gen_report: draft:bool -> unit -> unit
-end
diff --git a/src/plugins/markdown-report/md_gen.ml b/src/plugins/markdown-report/md_gen.ml
index f364f505685..b3d015cda0c 100644
--- a/src/plugins/markdown-report/md_gen.ml
+++ b/src/plugins/markdown-report/md_gen.ml
@@ -430,7 +430,8 @@ let gen_section_warnings env =
             Text [
               Plain "Note that this does not take into account emitted alarms:";
               Plain "they are reported in";
-              Link (plain "the next section", "#alarms")
+              Link (plain "the next section",
+                    Markdown.link_current_page "alarms")
             ]
           ];
           make_warnings_table warnings
@@ -446,7 +447,7 @@ let gen_section_alarms env =
   let treat_alarm e kf s ~rank:_ alarm annot (i, sec, content) =
     let kind = plain (Alarms.get_name alarm) in
     let label = "Alarm-" ^ string_of_int i in
-    let link = [Link (plain_format "%d" i, "#"^label)] in
+    let link = [Link (plain_format "%d" i, link_current_page label)] in
     let func = plain (Kernel_function.get_name kf) in
     let loc = string_of_loc (Cil_datatype.Stmt.loc s) in
     let loc_text = plain loc in
@@ -543,7 +544,7 @@ let gen_section_callgraph env =
         Block [
           Text [
             Plain "The image below shows the flamegraph (";
-            plain_link "http://www.brendangregg.com/flamegraphs.html";
+            plain_link (URL "http://www.brendangregg.com/flamegraphs.html");
             Plain ") for the chosen entry point."
           ]]
         :: Block [ Text [Image ("Flamegraph visualization.", f)] ]
@@ -571,12 +572,6 @@ let gen_alarms env =
   gen_section_callgraph env @
   gen_section_postlude env
 
-let mk_date () =
-  let tm = Unix.gmtime (Unix.time()) in
-  plain
-    (Printf.sprintf "%d-%02d-%02d"
-       (1900 + tm.Unix.tm_year) (1 + tm.Unix.tm_mon) tm.Unix.tm_mday)
-
 let mk_remarks is_draft =
   let f = Mdr_params.Remarks.get () in
   if f <> "" then Parse_remarks.get_remarks f
@@ -605,7 +600,6 @@ let gen_report ~draft:is_draft () =
     end else plain title
   in
   let authors = List.map (fun x -> plain x) (Mdr_params.Authors.get ()) in
-  let date = mk_date () in
   let elements = context @ coverage @ alarms in
   let elements =
     if is_draft then
@@ -628,7 +622,7 @@ let gen_report ~draft:is_draft () =
          "\\renewcommand{\\_}{\\discretionary{\\underscore}{}{\\underscore}}"]
    :: elements
   in
-  let doc = { title; authors; date; elements;} in
+  let doc = Markdown.pandoc ~title ~authors elements in
   try
     let out = open_out (Mdr_params.Output.get()) in
     let fmt = Format.formatter_of_out_channel out in
diff --git a/src/plugins/markdown-report/sarif.ml b/src/plugins/markdown-report/sarif.ml
index b6f0b7684b9..7bc383a632f 100644
--- a/src/plugins/markdown-report/sarif.ml
+++ b/src/plugins/markdown-report/sarif.ml
@@ -6,8 +6,8 @@
 
 module type Json_type = sig
   type t
-  val of_yojson: Yojson.Safe.json -> t Ppx_deriving_yojson_runtime.error_or
-  val to_yojson: t -> Yojson.Safe.json
+  val of_yojson: Yojson.Safe.t -> t Ppx_deriving_yojson_runtime.error_or
+  val to_yojson: t -> Yojson.Safe.t
 end
 
 module Json_dictionary(J: Json_type):
@@ -167,7 +167,7 @@ end
 
 module Custom_properties =
   Json_dictionary(struct
-    type t = Yojson.Safe.json
+    type t = Yojson.Safe.t
     let of_yojson x = Ok x
     let to_yojson x = x
   end)
@@ -808,8 +808,8 @@ sig
   val warning: t
   val error: t
 
-  val to_yojson: t -> Yojson.Safe.json
-  val of_yojson: Yojson.Safe.json -> (t,string) result
+  val to_yojson: t -> Yojson.Safe.t
+  val of_yojson: Yojson.Safe.t -> (t,string) result
 end
 =
 struct
diff --git a/src/plugins/markdown-report/sarif_gen.ml b/src/plugins/markdown-report/sarif_gen.ml
index ea8e9bd9b01..c87b44bf192 100644
--- a/src/plugins/markdown-report/sarif_gen.ml
+++ b/src/plugins/markdown-report/sarif_gen.ml
@@ -100,7 +100,7 @@ let gen_results remarks =
   rules, List.rev content
 
 let is_alarm = function
-  | Property.IPCodeAnnot (_,_,ca) -> Extlib.has_some (Alarms.find ca)
+  | Property.(IPCodeAnnot { ica_ca }) -> Extlib.has_some (Alarms.find ica_ca)
   | _ -> false
 
 let make_ip_message ip =
diff --git a/src/plugins/server/data.ml b/src/plugins/server/data.ml
index 796f4c54dce..c81c5ab623b 100644
--- a/src/plugins/server/data.ml
+++ b/src/plugins/server/data.ml
@@ -225,7 +225,7 @@ module Jtext =
 struct
   include Jany
   let syntax = Syntax.publish ~page:text_page ~name:"text"
-      ~synopsis:Syntax.any ~descr:(Markdown.rm "Formatted text.") ()
+      ~synopsis:Syntax.any ~descr:(Markdown.plain "Formatted text.") ()
 end
 
 (* -------------------------------------------------------------------------- *)
@@ -277,13 +277,13 @@ struct
       | Some v -> Fmap.add name (D.to_json v) r in
     { member ; getter ; setter }
 
-  let fields () = Syntax.fields ~title:"Field" !fdocs
+  let fields = Syntax.fields ~title:"Field" !fdocs
 
   let syntax =
     Syntax.publish ~page:R.page ~name:R.name
       ~descr:R.descr
       ~synopsis:(Syntax.record [])
-      ~details:(Markdown.mk_block fields) ()
+      ~details:[fields] ()
 
   let of_json js =
     List.fold_left
@@ -507,13 +507,17 @@ struct
           ) E.values
       end
 
-  let values () =
-    Markdown.table
-      [ `Left E.name ; `Left "Description" ]
-      (List.map
-         (fun (_,tag,descr) ->
-            [ Markdown.tt (Printf.sprintf "%S" tag) ; descr ]
-         ) E.values)
+  let values =
+    let open Markdown in
+    let caption = Some (plain "Values description") in
+    let header = [ plain E.name, Left; plain "Description", Left ] in
+    let content =
+      List.map
+        (fun (_,tag,descr) ->
+           [ [Markdown.Inline_code (Printf.sprintf "%S" tag)] ; descr ])
+        E.values
+    in
+    Table { caption; header; content }
 
   include Collection
       (struct
@@ -522,7 +526,7 @@ struct
         let syntax = Syntax.publish
             ~page:E.page ~name:E.name
             ~synopsis:Syntax.ident
-            ~descr:E.descr ~details:(Markdown.mk_block values) ()
+            ~descr:E.descr ~details:[values] ()
 
         let to_json value =
           register () ;
diff --git a/src/plugins/server/doc.ml b/src/plugins/server/doc.ml
index 0d141b3f018..e99ce3bd631 100644
--- a/src/plugins/server/doc.ml
+++ b/src/plugins/server/doc.ml
@@ -24,6 +24,7 @@
 (* --- Server Documentation                                               --- *)
 (* -------------------------------------------------------------------------- *)
 
+open Markdown
 type json = Yojson.Basic.t
 module Senv = Server_parameters
 module Pages = Map.Make(String)
@@ -36,8 +37,8 @@ type page = {
   chapter : chapter ;
   title : string ;
   order : int ;
-  intro : Markdown.section ;
-  mutable sections : Markdown.section list ;
+  intro : Markdown.elements ;
+  mutable sections : Markdown.elements list ;
 }
 
 let order = ref 0
@@ -45,7 +46,7 @@ let pages : page Pages.t ref = ref Pages.empty
 let plugins : string list ref = ref []
 let entries : (string * Markdown.href) list ref = ref []
 let path page = page.path
-let href page name : Markdown.href = `Section( page.path , name )
+let href page name : Markdown.href = Section( page.path , name )
 
 (* -------------------------------------------------------------------------- *)
 (* --- Page Collection                                                    --- *)
@@ -73,8 +74,8 @@ let page chapter ~title ~filename =
         Printf.sprintf "%s/%s/server/%s" Config.datadir name filename in
     let intro =
       if Sys.file_exists intro
-      then Markdown.read_section intro
-      else Markdown.(section ~title empty []) in
+      then [Markdown.raw_markdown intro]
+      else Markdown.(section ~title []) in
     let order = incr order ; !order in
     let page = { order ; rootdir ; path ;
                  chapter ; title ; intro ;
@@ -83,8 +84,8 @@ let page chapter ~title ~filename =
 
 let publish ~page ?name ?(index=[]) ~title content sections =
   let id = match name with Some id -> id | None -> title in
-  let href = `Section( page.path , id ) in
-  let section = Markdown.section ?name ~title content sections in
+  let href = Section( page.path , id ) in
+  let section = Markdown.section ?name ~title (content @ sections) in
   List.iter (fun entry -> entries := (entry , href) :: !entries) index ;
   page.sections <- section :: page.sections ; href
 
@@ -105,27 +106,25 @@ let pages_of_chapter c =
     (fun _ p -> if p.chapter = c then w := p :: !w) !pages ;
   List.sort (fun p q -> p.order - q.order) !w
 
-let table_of_chapter c fmt =
-  begin
-    Format.fprintf fmt "## %s@\n@." (title_of_chapter c) ;
-    List.iter
-      (fun p -> Format.fprintf fmt "   - [%s](%s)@." p.title p.path)
-      (pages_of_chapter c) ;
-    Format.pp_print_newline fmt () ;
-  end
-
-let table_of_contents fmt =
-  begin
-    table_of_chapter `Protocol fmt ;
-    table_of_chapter `Kernel fmt ;
-    List.iter
-      (fun p -> table_of_chapter (`Plugin p) fmt)
-      (List.sort String.compare !plugins)
-  end
+let table_of_chapter c =
+  [H2 (Markdown.plain (title_of_chapter c), None);
+   Block
+     [UL
+        (List.map
+           (fun p -> [Text [Link(Markdown.plain p.title, Page p.path)]])
+           (pages_of_chapter c))]]
+
+let table_of_contents () =
+  table_of_chapter `Protocol @
+  table_of_chapter `Kernel @
+  List.concat
+    (List.map
+       (fun p -> table_of_chapter (`Plugin p))
+       (List.sort String.compare !plugins))
 
 let index () =
   List.map
-    (fun (title,entry) -> Markdown.href ~title entry)
+    (fun (title,entry) -> Markdown.Link(plain title, entry))
     (List.sort (fun (a,_) (b,_) -> String.compare a b) !entries)
 
 let link ~toc ~title ~href : json =
@@ -162,13 +161,26 @@ let metadata page : json =
 (* --- Dump Documentation                                                 --- *)
 (* -------------------------------------------------------------------------- *)
 
+let pp_one_page ~root ~page ~title body =
+  let full_path = Filepath.normalize (root ^ "/" ^ page) in
+  let dir = Filename.dirname full_path in
+  Extlib.mkdir ~parents:true dir 0o755;
+  try
+    let chan = open_out full_path in
+    let fmt = Format.formatter_of_out_channel chan in
+    let title = plain title in
+    Markdown.(pp_pandoc fmt (pandoc ~title body))
+  with Sys_error e ->
+    Senv.fatal "Could not open file %s for writing: %s" full_path e
+
 let dump ~root ?(meta=true) () =
   begin
     Pages.iter
       (fun path page ->
          Senv.feedback "[doc] Page: '%s'" path ;
          let body = Markdown.subsections page.intro (List.rev page.sections) in
-         Markdown.dump ~root ~page:path (Markdown.document body) ;
+         let title = page.title in
+         pp_one_page ~root ~page:path ~title body ;
          if meta then
            let path = Printf.sprintf "%s/%s.json" root path in
            Yojson.Basic.to_file path (metadata page) ;
@@ -177,14 +189,17 @@ let dump ~root ?(meta=true) () =
     if meta then
       let path = Printf.sprintf "%s/readme.md.json" root in
       Yojson.Basic.to_file path maindata ;
-      Markdown.(dump ~root ~page:"readme.md"
-                  begin
-                    h1 "Documentation" </>
-                    par (bf "Version" <+> rm Config.version) </>
-                    fmt_block table_of_contents </>
-                    h2 "Index" </>
-                    list (index ())
-                  end) ;
+      let body =
+        [ H1 (plain "Documentation", None);
+          Block [Text [Bold "Version"; Plain Config.version]]]
+        @
+        table_of_contents ()
+        @
+        [H2 (plain "Index", None);
+         Block [UL (List.map (fun i -> [Text [i]]) (index ()))]]
+      in
+      let title = "Documentation" in
+      pp_one_page ~root ~page:"readme.md" ~title body
   end
 
 let () =
diff --git a/src/plugins/server/doc.mli b/src/plugins/server/doc.mli
index e204916f44b..e80db0a9a2a 100644
--- a/src/plugins/server/doc.mli
+++ b/src/plugins/server/doc.mli
@@ -56,9 +56,9 @@ val publish :
   ?name:string ->
   ?index:string list ->
   title:string ->
-  Markdown.block ->
-  Markdown.section list ->
-  href
+  Markdown.elements ->
+  Markdown.elements ->
+  Markdown.href
 
 (** Dumps all published pages of documentations. Unless [~meta:false],
     also generates METADATA for each page in
diff --git a/src/plugins/server/kernel_ast.ml b/src/plugins/server/kernel_ast.ml
index 77605bf53f3..9001f346b63 100644
--- a/src/plugins/server/kernel_ast.ml
+++ b/src/plugins/server/kernel_ast.ml
@@ -35,7 +35,7 @@ let page = Doc.page `Kernel ~title:"Ast Services" ~filename:"ast.md"
 
 let () = Request.register ~page
     ~kind:`EXEC ~name:"kernel.ast.compute"
-    ~descr:(Md.rm "Ensures that AST is computed")
+    ~descr:(Md.plain "Ensures that AST is computed")
     ~input:(module Junit) ~output:(module Junit) Ast.compute
 
 (* -------------------------------------------------------------------------- *)
@@ -104,7 +104,7 @@ module Stmt = Data.Collection
       type t = stmt
       let syntax = Sy.publish ~page ~name:"stmt"
           ~synopsis:Sy.ident
-          ~descr:(Md.rm "Code statement identifier") ()
+          ~descr:(Md.plain "Code statement identifier") ()
       let to_json st = `String (Tag.of_stmt st)
       let of_json js =
         let id = Js.to_string js in
@@ -134,7 +134,7 @@ module Kf = Data.Collection
       type t = kernel_function
       let syntax = Sy.publish ~page ~name:"fct-id"
           ~synopsis:Sy.ident
-          ~descr:(Md.rm "Function identified by its global name.") ()
+          ~descr:(Md.plain "Function identified by its global name.") ()
       let to_json kf =
         `String (Kernel_function.get_name kf)
       let of_json js =
@@ -149,7 +149,7 @@ module Kf = Data.Collection
 
 let () = Request.register ~page
     ~kind:`GET ~name:"kernel.ast.getFunctions"
-    ~descr:(Md.rm "Collect all functions in the AST")
+    ~descr:(Md.plain "Collect all functions in the AST")
     ~input:(module Junit) ~output:(module Kf.Jlist)
     begin fun () ->
       let pool = ref [] in
@@ -159,7 +159,7 @@ let () = Request.register ~page
 
 let () = Request.register ~page
     ~kind:`GET ~name:"kernel.ast.printFunction"
-    ~descr:(Md.rm "Print the AST of a function")
+    ~descr:(Md.plain "Print the AST of a function")
     ~input:(module Kf) ~output:(module Jtext)
     (fun kf -> Jbuffer.to_json PP.pp_global (Kernel_function.get_global kf))
 
diff --git a/src/plugins/server/kernel_main.ml b/src/plugins/server/kernel_main.ml
index 7448c53e42d..24bd58d36ad 100644
--- a/src/plugins/server/kernel_main.ml
+++ b/src/plugins/server/kernel_main.ml
@@ -38,15 +38,15 @@ let page = Doc.page `Kernel ~title:"Kernel Services" ~filename:"kernel.md"
 let () =
   let get_config = Request.signature
       ~page ~kind:`GET ~name:"kernel.getConfig"
-      ~descr:(Md.rm "Frama-C Kernel configuration")
+      ~descr:(Md.plain "Frama-C Kernel configuration")
       ~input:(module Junit) () in
   let result name descr =
-    Request.result get_config ~name ~descr:(Md.rm descr) (module Jstring) in
+    Request.result get_config ~name ~descr:(Md.plain descr) (module Jstring) in
   let set_version = result "version" "Frama-C version" in
   let set_datadir = result "datadir" "Shared directory (FRAMAC_SHARE)" in
   let set_libdir = result "libdir" "Lib directory (FRAMAC_LIB)" in
   let set_pluginpath = Request.result get_config
-      ~name:"pluginpath" ~descr:(Md.rm "Plugin directories (FRAMAC_PLUGIN)")
+      ~name:"pluginpath" ~descr:(Md.plain "Plugin directories (FRAMAC_PLUGIN)")
       (module Jstring.Jlist) in
   Request.register_sig get_config
     begin fun rq () ->
@@ -65,9 +65,10 @@ struct
   type t = Filepath.position
   let syntax = Sy.publish ~page ~name:"source"
       ~synopsis:(Sy.record [ "file" , Sy.string ; "line" , Sy.int ])
-      ~descr:(Md.rm "Source file positions.")
-      ~details:(Md.praw "The file path is normalized, \
-                         and the line number starts at one.") ()
+      ~descr:(Md.plain "Source file positions.")
+      ~details:Md.([Block [Text (plain "The file path is normalized, \
+                                        and the line number starts at one.")]])
+      ()
 
   let to_json p = `Assoc [
       "file" , `String (p.Filepath.pos_path :> string) ;
@@ -93,14 +94,14 @@ struct
   type t = Log.kind
   let page = page
   let name = "kind"
-  let descr = Md.rm "Frama-C message category."
+  let descr = Md.plain "Frama-C message category."
   let values = [
-    Log.Error,    "ERROR",    Md.rm "User Error" ;
-    Log.Warning,  "WARNING",  Md.rm "User Warning" ;
-    Log.Feedback, "FEEDBACK", Md.rm "Analyzer Feedback" ;
-    Log.Result,   "RESULT",   Md.rm "Analyzer Result" ;
-    Log.Failure,  "FAILURE",  Md.rm "Analyzer Failure" ;
-    Log.Debug,    "DEBUG",    Md.rm "Analyser Debug" ;
+    Log.Error,    "ERROR",    Md.plain "User Error" ;
+    Log.Warning,  "WARNING",  Md.plain "User Warning" ;
+    Log.Feedback, "FEEDBACK", Md.plain "Analyzer Feedback" ;
+    Log.Result,   "RESULT",   Md.plain "Analyzer Result" ;
+    Log.Failure,  "FAILURE",  Md.plain "Analyzer Failure" ;
+    Log.Debug,    "DEBUG",    Md.plain "Analyser Debug" ;
   ]
 end
 
@@ -117,12 +118,12 @@ struct
       (struct
         let page = page
         let name = "log"
-        let descr = Md.rm "Message event record."
+        let descr = Md.plain "Message event record."
       end)
 
   let syntax = R.syntax
 
-  let descr = Md.rm
+  let descr = Md.plain
   let kind = R.field "kind" ~descr:(descr "Message kind") (module LogKind)
   let plugin = R.field "plugin" ~descr:(descr "Emitter plugin") (module Jstring)
   let message = R.field "message" ~descr:(descr "Message text") (module Jstring)
@@ -195,12 +196,12 @@ let () =
 
 let () = Request.register
     ~page ~kind:`SET ~name:"kernel.setLogs"
-    ~descr:(Md.rm "Turn logs monitoring on/off")
+    ~descr:(Md.plain "Turn logs monitoring on/off")
     ~input:(module Jbool) ~output:(module Junit) monitor
 
 let () = Request.register
     ~page ~kind:`GET ~name:"kernel.getLogs"
-    ~descr:(Md.rm "Flush the last emitted logs since last call (max 100)")
+    ~descr:(Md.plain "Flush the last emitted logs since last call (max 100)")
     ~input:(module Junit) ~output:(module LogEvent.Jlist)
     begin fun () ->
       let pool = ref [] in
diff --git a/src/plugins/server/kernel_project.ml b/src/plugins/server/kernel_project.ml
index 883cea34f37..f2fd2ec4d9e 100644
--- a/src/plugins/server/kernel_project.ml
+++ b/src/plugins/server/kernel_project.ml
@@ -37,7 +37,7 @@ module ProjectInfo =
       type t = Project.t
 
       let syntax = Sy.publish ~page ~name:"project-info"
-          ~descr:(Md.rm "Project informations")
+          ~descr:(Md.plain "Project informations")
           ~synopsis:Sy.(record[ "id",ident; "name",string; "current",boolean ])
           ()
 
@@ -63,7 +63,7 @@ struct
 
   let syntax = Sy.publish ~page ~name:"project-request"
       ~synopsis:(Sy.(record[ "project",ident; "request",string; "data",any; ]))
-      ~descr:(Md.rm "Request to be executed on the specified project.") ()
+      ~descr:(Md.plain "Request to be executed on the specified project.") ()
 
   let of_json js =
     begin
@@ -86,37 +86,37 @@ end
 
 let () = Request.register ~page
     ~kind:`GET ~name:"kernel.project.getCurrent"
-    ~descr:(Md.rm "Returns the current project")
+    ~descr:(Md.plain "Returns the current project")
     ~input:(module Junit) ~output:(module ProjectInfo)
     Project.current
 
 let () = Request.register ~page
     ~kind:`SET ~name:"kernel.project.setCurrent"
-    ~descr:(Md.rm "Switches the current project")
+    ~descr:(Md.plain "Switches the current project")
     ~input:(module Jident) ~output:(module Junit)
     (fun pid -> Project.(set_current (from_unique_name pid)))
 
 let () = Request.register ~page
     ~kind:`GET ~name:"kernel.project.getList"
-    ~descr:(Md.rm "Returns the list of all projects")
+    ~descr:(Md.plain "Returns the list of all projects")
     ~input:(module Junit) ~output:(module ProjectInfo.Jlist)
     (fun () -> Project.fold_on_projects (fun ids p -> p :: ids) [])
 
 let () = Request.register ~page
     ~kind:`GET ~name:"kernel.project.getOn"
-    ~descr:(Md.rm "Execute a GET request within the given project")
+    ~descr:(Md.plain "Execute a GET request within the given project")
     ~input:(module ProjectRequest) ~output:(module Jany)
     (ProjectRequest.process `GET)
 
 let () = Request.register ~page
     ~kind:`SET ~name:"kernel.project.setOn"
-    ~descr:(Md.rm "Execute a SET request within the given project")
+    ~descr:(Md.plain "Execute a SET request within the given project")
     ~input:(module ProjectRequest) ~output:(module Jany)
     (ProjectRequest.process `SET)
 
 let () = Request.register ~page
     ~kind:`EXEC ~name:"kernel.project.execOn"
-    ~descr:(Md.rm "Execute an EXEC request within the given project")
+    ~descr:(Md.plain "Execute an EXEC request within the given project")
     ~input:(module ProjectRequest) ~output:(module Jany)
     (ProjectRequest.process `EXEC)
 
diff --git a/src/plugins/server/request.ml b/src/plugins/server/request.ml
index 039ee9f3c3c..7c042be0849 100644
--- a/src/plugins/server/request.ml
+++ b/src/plugins/server/request.ml
@@ -144,18 +144,18 @@ let sy_output (type b) (output : b rq_output) : Syntax.t =
   | Rfields _ -> Syntax.record []
 
 (* json input documentation *)
-let doc_input (type a) (input : a rq_input) : Markdown.block =
+let doc_input (type a) (input : a rq_input) =
   match input with
   | Pnone -> assert false
-  | Pdata _ -> Markdown.empty
-  | Pfields fs -> Syntax.fields ~title:"Input" (List.rev fs)
+  | Pdata _ -> []
+  | Pfields fs -> [Syntax.fields ~title:"Input" (List.rev fs)]
 
 (* json output syntax *)
-let doc_output (type b) (output : b rq_output) : Markdown.block =
+let doc_output (type b) (output : b rq_output) =
   match output with
   | Rnone -> assert false
-  | Rdata _ -> Markdown.empty
-  | Rfields fs -> Syntax.fields ~title:"Output" (List.rev fs)
+  | Rdata _ -> []
+  | Rfields fs -> [Syntax.fields ~title:"Output" (List.rev fs)]
 
 (* -------------------------------------------------------------------------- *)
 (* --- Multi-Parameters Requests                                          --- *)
@@ -253,8 +253,7 @@ let result_opt (type a b) (s : (a,unit) signature) ~name ~descr
 (* -------------------------------------------------------------------------- *)
 
 let signature
-    ~page ~kind ~name ~descr ?(details=Markdown.empty)
-    ?input ?output () =
+    ~page ~kind ~name ~descr ?(details=[]) ?input ?output () =
   check_name name ;
   check_page page name ;
   check_kind kind name ;
@@ -299,6 +298,7 @@ let mk_output (type b) name required (output : b rq_output) : (rq -> b -> json)
        fmap_to_json rq.result)
 
 let register_sig (type a b) (s : (a,b) signature) (process : rq -> a -> b) =
+  let open Markdown in
   if s.defined then
     Senv.fatal "Request '%s' is defined twice" s.name ;
   let input = mk_input s.name s.defaults s.input in
@@ -309,19 +309,18 @@ let register_sig (type a b) (s : (a,b) signature) (process : rq -> a -> b) =
   in
   let skind = Main.string_of_kind s.kind in
   let title =  Printf.sprintf "`%s` %s" skind s.name in
-  let synopsis =
-    Markdown.table
-      [`Center "Input" ; `Center "Output" ]
-      [[ Syntax.format @@ sy_input s.input ;
-         Syntax.format @@ sy_output s.output ]] in
+  let caption = Some s.descr in
+  let header = [ plain "Input", Center; plain "Output", Center] in
   let content =
-    Markdown.concat [
-      Markdown.par s.descr ;
-      synopsis ;
-      s.details ;
-      doc_input s.input ;
-      doc_output s.output ;
-    ] in
+    [[ Syntax.format @@ sy_input s.input ;
+       Syntax.format @@ sy_output s.output ]]
+  in
+  let synopsis = Table { caption; header; content } in
+  let content =
+    [ synopsis ; Block s.details] @
+      doc_input s.input @
+      doc_output s.output
+  in
   let _ = Doc.publish ~page:s.page ~name:s.name ~title content [] in
   Main.register s.kind s.name processor ;
   s.defined <- true
diff --git a/src/plugins/server/syntax.ml b/src/plugins/server/syntax.ml
index 565a227cde5..fc97bc44c6f 100644
--- a/src/plugins/server/syntax.ml
+++ b/src/plugins/server/syntax.ml
@@ -58,55 +58,59 @@ let flow md = { atomic=false ; text=md }
 
 let format { text } = text
 let protect a =
-  if a.atomic then a.text else Markdown.(rm "(" <+> a.text <+> rm ")")
+  if a.atomic then a.text else Markdown.((Plain "(") :: a.text @ [Plain ")"])
 
-let publish ~page ~name ~descr ~synopsis ?(details = Markdown.empty) () =
+let publish ~page ~name ~descr ~synopsis ?(details = []) () =
   check_name name ;
   check_page page name ;
   let id = Printf.sprintf "data-%s" name in
   let title = Printf.sprintf "`DATA` %s" name in
-  let format = ref Markdown.nil in
-  let syntax = Markdown.fmt_block (fun fmt ->
-      Format.fprintf fmt "> %a ::= %a"
-        Markdown.pp_text !format
-        Markdown.pp_text synopsis.text
-    ) in
-  let content = Markdown.( par descr </> syntax </> details ) in
-  let href = Doc.publish ~page ~name:id ~title ~index:[name] content [] in
+  let href = Doc.href page id in
   let link_title = Printf.sprintf "_%s_" name in
-  let link = Markdown.href ~title:link_title href in
-  format := link ; atom @@ link
-
-let unit = atom @@ Markdown.rm "-"
-let any = atom @@ Markdown.it "any"
-let int = atom @@ Markdown.it "int"
-let ident = atom @@ Markdown.it "ident"
-let string = atom @@ Markdown.it "string"
-let number = atom @@ Markdown.it "number"
-let boolean = atom @@ Markdown.it "boolean"
-
-let escaped name = Markdown.tt @@ Printf.sprintf "'%s'" @@ String.escaped name
-
-let tag name = atom @@ escaped name
-
-let array a = atom @@ Markdown.(tt "[" <+> protect a <+> tt ", … ]")
+  let link = Markdown.(Link (plain link_title, href)) in
+  let syntax =
+    Markdown.(
+      Text
+        (Plain ">" :: link :: Plain "::=" :: synopsis.text))
+  in
+  let content = Markdown.((Block [Text descr; syntax]) :: details) in
+  let _href = Doc.publish ~page ~name:id ~title ~index:[name] content [] in
+  atom @@ [link]
+
+let unit = atom @@ [Markdown.Plain "-"]
+let any = atom @@ [Markdown.Emph "any"]
+let int = atom @@ [Markdown.Emph "int"]
+let ident = atom @@ [Markdown.Emph "ident"]
+let string = atom @@ [Markdown.Emph "string"]
+let number = atom @@ [Markdown.Emph "number"]
+let boolean = atom @@ [Markdown.Emph "boolean"]
+
+let escaped name =
+  Markdown.Inline_code (Printf.sprintf "'%s'" @@ String.escaped name)
+
+let tag name = atom @@ [escaped name]
+
+let array a =
+  atom @@ Markdown.(Inline_code "[" :: protect a @ [Inline_code  ", … ]"])
 
 let tuple ts =
-  atom @@ Markdown.(tt "["
-                    <+> glue ~sep:(raw " `,` ") (List.map protect ts) <+>
-                    tt "]")
+  atom @@
+  Markdown.(
+    Inline_code "[" ::
+    glue ~sep:[Inline_code ","] (List.map protect ts) @
+    [Inline_code "]"])
 
-let union ts = flow @@ Markdown.(glue ~sep:(raw " | ") (List.map protect ts))
+let union ts = flow @@ Markdown.(glue ~sep:(plain "|") (List.map protect ts))
 
-let option t = atom @@ Markdown.(protect t <@> tt "?")
+let option t = atom @@ Markdown.(protect t @ [Inline_code "?"])
 
-let field (a,t) = Markdown.( escaped a <+> tt ":" <+> t.text )
+let field (a,t) = Markdown.( escaped a :: Inline_code ":" :: t.text )
 
 let record fds =
   let fields =
-    if fds = [] then Markdown.rm "…" else
-      Markdown.(glue ~sep:(raw " `;` ") (List.map field fds))
-  in atom @@ Markdown.(tt "{" <+> fields <+> tt "}")
+    if fds = [] then Markdown.plain "…" else
+      Markdown.(glue ~sep:[Inline_code ";"] (List.map field fds))
+  in atom @@ Markdown.(Inline_code "{" :: fields @ [Inline_code "}"])
 
 type field = {
   name : string ;
@@ -115,15 +119,15 @@ type field = {
 }
 
 let fields ~title (fds : field list) =
-  let c_field = `Left title in
-  let c_format = `Center "Format" in
-  let c_descr = `Left "Description" in
-  Markdown.table [ c_field ; c_format ; c_descr ]
-    begin
-      List.map
-        (fun f ->
-           [ Markdown.tt f.name ; format f.syntax ; f.descr ])
-        fds
-    end
+  let open Markdown in
+  let caption = Some (plain "Fields description") in
+  let header =
+    [plain title, Left; plain "Format", Center; plain "Description", Left]
+  in
+  let content =
+    List.map
+      (fun f -> [[Markdown.Inline_code f.name]; format f.syntax ; f.descr]) fds
+  in
+  Markdown.Table { caption; header; content }
 
 (* -------------------------------------------------------------------------- *)
diff --git a/src/plugins/server/syntax.mli b/src/plugins/server/syntax.mli
index ff4c575e6c3..c56312909ed 100644
--- a/src/plugins/server/syntax.mli
+++ b/src/plugins/server/syntax.mli
@@ -33,7 +33,7 @@ val format : t -> Markdown.text
     the description block. *)
 val publish :
   page:Doc.page -> name:string -> descr:Markdown.text ->
-  synopsis:t -> ?details:Markdown.block -> unit -> t
+  synopsis:t -> ?details:Markdown.elements -> unit -> t
 
 val unit : t
 val any : t
@@ -54,6 +54,6 @@ type field = { name : string ; syntax : t ; descr : Markdown.text }
 
 (** Builds a table with fields column named with [~title]
     (shall be capitalized) *)
-val fields : title:string -> field list -> Markdown.block
+val fields : title:string -> field list -> Markdown.element
 
 (* -------------------------------------------------------------------------- *)
-- 
GitLab