(*---------------------------------------------------------------------------
Copyright (c) 2021 The cmarkit programmers. All rights reserved.
Distributed under the ISC license, see terms at the end of the file.
---------------------------------------------------------------------------*)
open Cmarkit
module C = Cmarkit_renderer.Context
module String_set = Set.Make (String)
(* Renderer state *)
type state =
{ safe : bool;
backend_blocks : bool;
mutable ids : String_set.t;
mutable footnote_count : int;
mutable footnotes :
(* Text, id, ref count, footnote *)
(string * string * int ref * Block.Footnote.t) Label.Map.t }
let state : state C.State.t = C.State.make ()
let safe c = (C.State.get c state).safe
let backend_blocks c = (C.State.get c state).backend_blocks
let init_context ?(backend_blocks = false) ~safe c _ =
let ids = String_set.empty and footnotes = Label.Map.empty in
let st = { safe; backend_blocks; ids; footnote_count = 0; footnotes } in
C.State.set c state (Some st)
let unique_id c id =
let st = C.State.get c state in
let rec loop ids id c =
let id' = if c = 0 then id else (String.concat "-" [id; Int.to_string c]) in
match String_set.mem id' ids with
| true -> loop ids id (c + 1)
| false -> st.ids <- String_set.add id' ids; id'
in
loop st.ids id 0
let footnote_id label =
let make_label l = String.map (function ' ' | '\t' -> '-' | c -> c) l in
"fn-" ^ (make_label (String.sub label 1 (String.length label - 1)))
let footnote_ref_id fnid c = String.concat "-" ["ref"; Int.to_string c; fnid]
let make_footnote_ref_ids c label fn =
let st = C.State.get c state in
match Label.Map.find_opt label st.footnotes with
| Some (text, id, refc, _) -> incr refc; (text, id, footnote_ref_id id !refc)
| None ->
st.footnote_count <- st.footnote_count + 1;
let text = String.concat "" ["["; Int.to_string st.footnote_count;"]"] in
let id = footnote_id label in
st.footnotes <- Label.Map.add label (text, id, ref 1, fn) st.footnotes;
text, id, footnote_ref_id id 1
(* Escaping *)
let buffer_add_html_escaped_uchar b u = match Uchar.to_int u with
| 0x0000 -> Buffer.add_utf_8_uchar b Uchar.rep
| 0x0026 (* & *) -> Buffer.add_string b "&"
| 0x003C (* < *) -> Buffer.add_string b "<"
| 0x003E (* > *) -> Buffer.add_string b ">"
(* | 0x0027 (* ' *) -> Buffer.add_string b "'" *)
| 0x0022 (* '\"' *) -> Buffer.add_string b """
| _ -> Buffer.add_utf_8_uchar b u
let html_escaped_uchar c s = buffer_add_html_escaped_uchar (C.buffer c) s
let buffer_add_html_escaped_string b s =
let string = Buffer.add_string in
let len = String.length s in
let max_idx = len - 1 in
let flush b start i =
if start < len then Buffer.add_substring b s start (i - start);
in
let rec loop start i =
if i > max_idx then flush b start i else
let next = i + 1 in
match String.get s i with
| '\x00' ->
flush b start i; Buffer.add_utf_8_uchar b Uchar.rep; loop next next
| '&' -> flush b start i; string b "&"; loop next next
| '<' -> flush b start i; string b "<"; loop next next
| '>' -> flush b start i; string b ">"; loop next next
(* | '\'' -> flush c start i; string c "'"; loop next next *)
| '\"' -> flush b start i; string b """; loop next next
| c -> loop start next
in
loop 0 0
let html_escaped_string c s = buffer_add_html_escaped_string (C.buffer c) s
let buffer_add_pct_encoded_string b s = (* Percent encoded + HTML escaped *)
let byte = Buffer.add_char and string = Buffer.add_string in
let unsafe_hexdig_of_int i = match i < 10 with
| true -> Char.unsafe_chr (i + 0x30)
| false -> Char.unsafe_chr (i + 0x37)
in
let flush b max start i =
if start <= max then Buffer.add_substring b s start (i - start);
in
let rec loop b s max start i =
if i > max then flush b max start i else
let next = i + 1 in
match String.get s i with
| '%' (* In CommonMark destinations may have percent encoded chars *)
(* See https://tools.ietf.org/html/rfc3986 *)
(* unreserved *)
| 'A' .. 'Z' | 'a' .. 'z' | '0' .. '9' | '-' | '.' | '_' | '~'
(* sub-delims *)
| '!' | '$' | (*'&' | '\'' | *) '(' | ')' | '*' | '+' | ',' | ';' | '='
(* gen-delims *)
| ':' | '/' | '?' | '#' | (* '[' | ']' cmark escapes them | *) '@' ->
loop b s max start next
| '&' -> flush b max start i; string b "&"; loop b s max next next
| '\'' -> flush b max start i; string b "'"; loop b s max next next
| c ->
flush b max start i;
let hi = (Char.code c lsr 4) land 0xF in
let lo = (Char.code c) land 0xF in
byte b '%';
byte b (unsafe_hexdig_of_int hi);
byte b (unsafe_hexdig_of_int lo);
loop b s max next next
in
loop b s (String.length s - 1) 0 0
let pct_encoded_string c s = buffer_add_pct_encoded_string (C.buffer c) s
(* Rendering functions *)
let comment c s =
C.string c ""
let comment_undefined_label c l = match Inline.Link.referenced_label l with
| None -> () | Some def -> comment c ("Undefined label " ^ (Label.key def))
let comment_unknown_def_type c l = match Inline.Link.referenced_label l with
| None -> () | Some def ->
comment c ("Unknown label definition type for " ^ (Label.key def))
let comment_foonote_image c l = match Inline.Link.referenced_label l with
| None -> () | Some def ->
comment c ("Footnote " ^ (Label.key def) ^ " referenced as image")
let block_lines c = function (* newlines only between lines *)
| [] -> () | (l, _) :: ls ->
let line c (l, _) = C.byte c '\n'; C.string c l in
C.string c l; List.iter (line c) ls
(* Inline rendering *)
let autolink c a =
let pre = if Inline.Autolink.is_email a then "mailto:" else "" in
let url = pre ^ (fst (Inline.Autolink.link a)) in
let url = if Inline.Link.is_unsafe url then "" else url in
C.string c "";
html_escaped_string c (fst (Inline.Autolink.link a));
C.string c ""
let break c b = match Inline.Break.type' b with
| `Hard -> C.string c "
\n"
| `Soft -> C.byte c '\n'
let code_span c cs =
C.string c "";
html_escaped_string c (Inline.Code_span.code cs);
C.string c ""
let emphasis c e =
C.string c ""; C.inline c (Inline.Emphasis.inline e); C.string c ""
let strong_emphasis c e =
C.string c "";
C.inline c (Inline.Emphasis.inline e);
C.string c ""
let link_dest_and_title c ld =
let dest = match Link_definition.dest ld with
| None -> ""
| Some (link, _) when safe c && Inline.Link.is_unsafe link -> ""
| Some (link, _) -> link
in
let title = match Link_definition.title ld with
| None -> ""
| Some title -> String.concat "\n" (List.map (fun (_, (t, _)) -> t) title)
in
dest, title
let image ?(close = " >") c i =
match Inline.Link.reference_definition (C.get_defs c) i with
| Some (Link_definition.Def (ld, _)) ->
let plain_text c i =
let lines = Inline.to_plain_text ~break_on_soft:false i in
String.concat "\n" (List.map (String.concat "") lines)
in
let link, title = link_dest_and_title c ld in
C.string c " ""
then (C.string c " title=\""; html_escaped_string c title; C.byte c '\"');
C.string c close
| Some (Block.Footnote.Def _) -> comment_foonote_image c i
| None -> comment_undefined_label c i
| Some _ -> comment_unknown_def_type c i
let link_footnote c l fn =
let key = Label.key (Option.get (Inline.Link.referenced_label l)) in
let text, label, ref = make_footnote_ref_ids c key fn in
let is_full_ref = match Inline.Link.reference l with
| `Ref (`Full, _, _) -> true | _ -> false
in
if is_full_ref then begin
C.string c "";
C.inline c (Inline.Link.text l); C.string c ""
end else begin
C.string c "";
C.string c text; C.string c ""
end
let link c l = match Inline.Link.reference_definition (C.get_defs c) l with
| Some (Link_definition.Def (ld, _)) ->
let link, title = link_dest_and_title c ld in
C.string c " "" then (C.string c "\" title=\""; html_escaped_string c title);
C.string c "\">"; C.inline c (Inline.Link.text l); C.string c ""
| Some (Block.Footnote.Def (fn, _)) -> link_footnote c l fn
| None -> C.inline c (Inline.Link.text l); comment_undefined_label c l
| Some _ -> C.inline c (Inline.Link.text l); comment_unknown_def_type c l
let raw_html c h =
if safe c then comment c "CommonMark raw HTML omitted" else
let line c (_, (h, _)) = C.byte c '\n'; C.string c h in
if h <> []
then (C.string c (fst (snd (List.hd h))); List.iter (line c) (List.tl h))
let strikethrough c s =
C.string c "
";
C.inline c (Inline.Strikethrough.inline s);
C.string c ""
let math_span c ms =
let tex_line c l = html_escaped_string c (Block_line.tight_to_string l) in
let tex_lines c = function (* newlines only between lines *)
| [] -> () | l :: ls ->
let line c l = C.byte c '\n'; tex_line c l in
tex_line c l; List.iter (line c) ls
in
let tex = Inline.Math_span.tex_layout ms in
if tex = [] then () else
(C.string c (if Inline.Math_span.display ms then "\\[" else "\\(");
tex_lines c tex;
C.string c (if Inline.Math_span.display ms then "\\]" else "\\)"))
let inline c = function
| Inline.Autolink (a, _) -> autolink c a; true
| Inline.Break (b, _) -> break c b; true
| Inline.Code_span (cs, _) -> code_span c cs; true
| Inline.Emphasis (e, _) -> emphasis c e; true
| Inline.Image (i, _) -> image c i; true
| Inline.Inlines (is, _) -> List.iter (C.inline c) is; true
| Inline.Link (l, _) -> link c l; true
| Inline.Raw_html (html, _) -> raw_html c html; true
| Inline.Strong_emphasis (e, _) -> strong_emphasis c e; true
| Inline.Text (t, _) -> html_escaped_string c t; true
| Inline.Ext_strikethrough (s, _) -> strikethrough c s; true
| Inline.Ext_math_span (ms, _) -> math_span c ms; true
| _ -> comment c ""; true
(* Block rendering *)
let block_quote c bq =
C.string c "
\n"; C.block c (Block.Block_quote.block bq); C.string c "\n" let code_block c cb = let i = Option.map fst (Block.Code_block.info_string cb) in let lang = Option.bind i Block.Code_block.language_of_info_string in let line (l, _) = html_escaped_string c l; C.byte c '\n' in match lang with | Some (lang, _env) when backend_blocks c && lang.[0] = '=' -> if lang = "=html" && not (safe c) then block_lines c (Block.Code_block.code cb) else () | _ -> C.string c "
()
| Some (lang, _env) ->
C.string c " class=\"language-"; html_escaped_string c lang;
C.byte c '\"'
end;
C.byte c '>';
List.iter line (Block.Code_block.code cb);
C.string c "\n"
let heading c h =
let level = string_of_int (Block.Heading.level h) in
C.string c ""; C.inline c (Block.Paragraph.inline p); C.string c "
\n" let item_block ~tight c = function | Block.Blank_line _ -> () | Block.Paragraph (p, _) when tight -> C.inline c (Block.Paragraph.inline p) | Block.Blocks (bs, _) -> let rec loop c add_nl = function | Block.Blank_line _ :: bs -> loop c add_nl bs | Block.Paragraph (p,_) :: bs when tight -> C.inline c (Block.Paragraph.inline p); loop c true bs | b :: bs -> (if add_nl then C.byte c '\n'); C.block c b; loop c false bs | [] -> () in loop c true bs | b -> C.byte c '\n'; C.block c b let list_item ~tight c (i, _) = match Block.List_item.ext_task_marker i with | None -> C.string c "