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 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
|
(**************************************************************************)
(* *)
(* OCamlFormat *)
(* *)
(* Copyright (c) Facebook, Inc. and its affiliates. *)
(* *)
(* This source code is licensed under the MIT license found in *)
(* the LICENSE file in the root directory of this source tree. *)
(* *)
(**************************************************************************)
open Fmt
open Ocamlformat_odoc_parser
open Ocamlformat_odoc_parser.Ast
module Loc = Ocamlformat_odoc_parser.Loc
type fmt_code =
Conf.t
-> offset:int
-> set_margin:bool
-> string
-> (Fmt.t, [`Msg of string]) Result.t
type c = {fmt_code: fmt_code; conf: Conf.t}
(** Escape characters if they are not already escaped. [escapeworthy] should
be [true] if the character should be escaped, [false] otherwise. *)
let ensure_escape ?(escape_char = '\\') ~escapeworthy s =
let dst = Buffer.create (String.length s + 8) in
let prev_off = ref 0 in
let stash until =
Buffer.add_substring dst s ~pos:!prev_off ~len:(until - !prev_off)
in
let len = String.length s in
for i = 0 to len - 1 do
if
escapeworthy s.[i]
&& not (String.Escaping.is_char_escaped s ~escape_char i)
then (
stash i ;
prev_off := i ;
Buffer.add_char dst escape_char )
done ;
stash len ;
Buffer.contents dst
(** Insert [ins] into [s] at every indexes in [ats]. *)
let insert_ats s ins ats =
let len = String.length s in
let b = Buffer.create (len + (String.length ins * List.length ats)) in
let stash pos until = Buffer.add_substring b s ~pos ~len:(until - pos) in
let rec loop last_ins = function
| [] -> stash last_ins len
| i :: tl -> stash last_ins i ; Buffer.add_string b ins ; loop i tl
in
loop 0 (List.sort ~compare:Int.compare ats) ;
Buffer.contents b
let escape_balanced_brackets s =
(* Do not escape paired brackets. Opening and closing that couldn't be
paired will be escaped. *)
let rec brackets_to_escape opens closes i =
if i >= String.length s then opens @ closes
else
let opens, closes =
match s.[i] with
| '[' -> (i :: opens, closes)
| ']' -> (
match opens with
| [] -> (opens, i :: closes)
| _ :: tl -> (tl, closes) )
| _ -> (opens, closes)
in
brackets_to_escape opens closes (i + 1)
in
insert_ats s "\\" (brackets_to_escape [] [] 0)
let escape_all s =
let escapeworthy = function '{' | '}' | '[' | ']' -> true | _ -> false in
ensure_escape ~escapeworthy s
let split_on_whitespaces =
String.split_on_chars ~on:['\t'; '\n'; '\011'; '\012'; '\r'; ' ']
(** Escape special characters and normalize whitespaces *)
let str_normalized ?(escape = escape_all) ~wrap s =
if wrap then
split_on_whitespaces s
|> List.filter ~f:(Fn.non String.is_empty)
|> fun s -> list s space_break (fun s -> escape s |> str)
else str (escape s)
let rec drop_leading_spaces = function
| {Loc.value= `Space _; _} :: tl -> drop_leading_spaces tl
| elems -> elems
let ign_loc ~f with_loc = f with_loc.Loc.value
(** Format the content of code and verbatim blocks. *)
let fmt_multiline_text txt =
let fmt_line ~first ~last:_ l =
let l = String.rstrip l in
if first then str l
else if String.length l = 0 then str_as 0 "\n"
else force_break $ str l
in
let lines = String.split_lines txt in
vbox 0 (list_fl lines fmt_line)
let fmt_verbatim_block ~loc s =
let content =
let s, _warnings = Odoc_parser.verbatim_content loc s in
fmt_multiline_text s
in
hvbox 0 (str "{v" $ space_break $ content $ space_break $ str "v}")
let fmt_code_span ~wrap s =
let s = escape_balanced_brackets s in
let s =
if wrap then
let words = String.split_on_chars ~on:[' '] s in
list words space_break str
else str s
in
hovbox_if wrap 1 (str "[" $ s $ str "]")
let fmt_math_span s = hovbox 2 (wrap (str "{m ") (str "}") (str s))
let fmt_math_block s =
let lines =
List.drop_while ~f:String.is_empty
@@ List.rev
@@ List.drop_while ~f:String.is_empty
@@ List.rev_map ~f:String.strip
@@ String.split_lines s
in
let fmt ~first ~last:_ line =
if first then str line
else if String.is_empty line then str "\n"
else break 1000 0 $ str line
in
hvbox 2
(wrap
(str "{math" $ space_break)
(break 0 (-2) $ str "}")
(list_fl lines fmt) )
let fmt_reference = ign_loc ~f:str
(* Decide between using light and heavy syntax for lists *)
let list_should_use_heavy_syntax items =
let heavy_nestable_block_elements = function
(* More than one element or contains a list *)
| [{Loc.value= `List _; _}] | _ :: _ :: _ -> true
| [] | [_] -> false
in
List.exists items ~f:heavy_nestable_block_elements
(* Decide if a blank line should be added between two elements *)
let block_element_should_blank elem next =
match (elem, next) with
| `Tag _, `Tag _ -> false
(* Mandatory blanks lines. *)
| (`List _ | `Tag _), _ | `Paragraph _, `Paragraph _ -> true
| _, _ -> false
let should_preserve_blank _c (a : Loc.span) (b : Loc.span) =
(* Whether there were already an empty line *)
b.start.line - a.end_.line > 1
(* Format a list of block_elements separated by newlines Inserts blank line
depending on [block_element_should_blank] *)
let list_block_elem c elems f =
list_pn elems (fun ~prev:_ elem ~next ->
let break =
match next with
| Some n ->
if
block_element_should_blank
(elem.Loc.value :> block_element)
(n.value :> block_element)
|| should_preserve_blank c elem.location n.location
then str "\n" $ force_break
else force_break
| None -> noop
in
f elem $ break )
module Light_table = struct
(** A table type that can safely be formatted using the light syntax. *)
type cell = inline_element with_location list
type row = cell list
(** [header_rows, alignments, data_rows] *)
type t = row list * alignment option list option * row list
module Ast = Ocamlformat_odoc_parser.Ast
(** Returns [None] if the given table cannot be safely formatted using the
light syntax. This might return [None] for tables that were using the
light syntax in the original source. *)
let of_table : Ast.table -> t option =
let exception Table_not_safe in
let extract_cell ((elems, _header) : _ Ast.cell) =
match elems with
| [] -> []
| [{value= `Paragraph inline_elems; _}] -> inline_elems
| _ -> raise Table_not_safe
in
let extract_row (row : _ Ast.row) = List.map ~f:extract_cell row in
let is_header_cell (_, h) =
match h with `Header -> true | `Data -> false
in
let rec extract header : _ Ast.grid -> _ = function
| hd :: tl when List.exists hd ~f:is_header_cell ->
extract (extract_row hd :: header) tl
| data_rows -> (List.rev header, List.map ~f:extract_row data_rows)
in
function
| _, `Heavy -> None
| (grid, alignments), `Light -> (
try
let header, data = extract [] grid in
Some (header, alignments, data)
with Table_not_safe -> None )
let alignment_chars = function
| Some `Left -> (":", "-")
| Some `Center -> (":", ":")
| Some `Right -> ("-", ":")
| None -> ("-", "-")
end
let non_wrap_space sp =
if String.contains sp '\n' then force_newline else str sp
let fmt_block_markup ?force_break:(fb = false) tag content =
let initial_break = if fb then force_break else space_break in
hvbox 2
(str "{" $ str tag $ initial_break $ content $ break 1 ~-2 $ str "}")
let rec fmt_inline_elements c ~wrap elements =
let rec aux = function
| [] -> noop
| `Space sp :: `Word (("-" | "+") as w) :: t ->
(* Escape lines starting with '+' or '-'. *)
fmt_or wrap
(cbreak ~fits:("", 1, "") ~breaks:("", 0, "\\"))
(non_wrap_space sp)
$ str w $ aux t
| `Space sp :: t -> fmt_or wrap space_break (non_wrap_space sp) $ aux t
| `Word w :: t ->
fmt_if (String.is_prefix ~prefix:"@" w) (str "\\")
$ str_normalized ~wrap w $ aux t
| `Code_span s :: t -> fmt_code_span ~wrap s $ aux t
| `Math_span s :: t -> fmt_math_span s $ aux t
| `Raw_markup (lang, s) :: t ->
let lang =
match lang with
| Some l -> str_normalized ~wrap l $ str ":"
| None -> noop
in
(* todo check this was an escape sequence *)
str "{%" $ (lang $ str s) $ str "%}" $ aux t
| `Styled (style, elems) :: t ->
let s =
match style with
| `Bold -> "b"
| `Italic -> "i"
| `Emphasis -> "e"
| `Superscript -> "^"
| `Subscript -> "_"
in
hovbox_if wrap
(1 + String.length s + 1)
(fmt_markup_with_inline_elements c ~wrap ~force_space:true
(str_normalized ~wrap s) elems )
$ aux t
| `Reference (_kind, rf, txt) :: t ->
let rf = str "{!" $ fmt_reference rf $ str "}" in
fmt_link_or_reference c ~wrap rf txt $ aux t
| `Link (url, txt) :: t ->
let url = str "{:" $ str_normalized ~wrap url $ str "}" in
fmt_link_or_reference c ~wrap url txt $ aux t
in
aux (List.map elements ~f:(ign_loc ~f:Fn.id))
and fmt_link_or_reference c ~wrap tag txt =
match txt with
| [] -> tag
| _ :: _ ->
hovbox_if wrap 1 (fmt_markup_with_inline_elements c ~wrap tag txt)
(** Format a markup of the form [{tag elems}]. If [force_space] is [true], a
space will be added after the tag, even if it's not present in the source.
*)
and fmt_markup_with_inline_elements c ~wrap ?(force_space = false) tag elems
=
let leading_space, elems =
if force_space then (str " ", drop_leading_spaces elems)
else (noop, elems)
in
str "{" $ tag $ leading_space $ fmt_inline_elements c ~wrap elems $ str "}"
and fmt_nestable_block_element c (elm : nestable_block_element with_location)
=
match elm.Loc.value with
| `Paragraph elems ->
hovbox 0
(fmt_inline_elements c ~wrap:c.conf.fmt_opts.wrap_docstrings.v elems)
| `Code_block code_block -> fmt_code_block ~loc:elm.location c code_block
| `Math_block s -> fmt_math_block s
| `Verbatim s -> fmt_verbatim_block ~loc:elm.location s
| `Modules mods ->
hovbox 0
(wrap
(str "{!modules:" $ cut_break)
(cut_break $ str "}")
(list mods space_break (fun ref -> fmt_reference ref)) )
| `List (k, _syntax, items) when list_should_use_heavy_syntax items ->
fmt_list_heavy c k items
| `List (k, _syntax, items) -> fmt_list_light c k items
| `Table table -> fmt_table c table
| `Media (_kind, href, text, media) -> (
let prefix =
match media with
| `Image -> "image"
| `Video -> "video"
| `Audio -> "audio"
in
let href =
match href.value with
| `Reference s -> str "!" $ str s
| `Link s -> str ":" $ str s
in
let ref = str "{" $ str prefix $ href $ str "}" in
match text with "" -> ref | _ -> str "{" $ ref $ str text $ str "}" )
and fmt_list_heavy c kind items =
let fmt_item elems =
let box = match elems with [_] -> hvbox 3 | _ -> vbox 3 in
box
(wrap (str "{- ")
(break 1 (-3) $ str "}")
(fmt_nestable_block_elements c elems) )
and start =
match kind with
| `Unordered -> str "{ul" $ cut_break
| `Ordered -> str "{ol" $ cut_break
in
vbox 1
(wrap start (break 1 (-1) $ str "}") (list items cut_break fmt_item))
and fmt_list_light c kind items =
let line_start =
match kind with `Unordered -> str "- " | `Ordered -> str "+ "
in
let fmt_item elems =
line_start $ hovbox 0 (fmt_nestable_block_elements c elems)
in
vbox 0 (list items force_break fmt_item)
and fmt_table_heavy c (((grid, alignments), _) : table) =
let fmt_cell (elems, header) =
let cell_tag = match header with `Header -> "th" | `Data -> "td" in
fmt_block_markup cell_tag (fmt_nestable_block_elements c elems)
in
let fmt_row row = fmt_block_markup "tr" (list row space_break fmt_cell) in
ignore alignments ;
fmt_block_markup "table" (list grid force_break fmt_row)
and fmt_table_light c (header, alignments, data) =
(* Format every cells into strings to then compute the width of columns. *)
let format_rows_to_strings =
let format_cell_to_string elems =
Format_.asprintf " %a " Fmt.eval
@@ fmt_inline_elements c ~wrap:false elems
in
List.map ~f:(List.map ~f:format_cell_to_string)
in
let header = format_rows_to_strings header
and data = format_rows_to_strings data in
let column_width =
let column_count =
let f acc row = max acc (List.length row) in
let compute init rows = List.fold_left rows ~init ~f in
let aligns_count =
Option.value_map alignments ~default:0 ~f:List.length
in
compute (compute aligns_count header) data
in
let column_min_width = if Option.is_some alignments then 3 else 1 in
let widths = Array.init column_count ~f:(fun _ -> column_min_width) in
let compute_column_widths row =
List.iteri row ~f:(fun i cell ->
widths.(i) <- max widths.(i) (Fmt.str_length cell) )
in
List.iter ~f:compute_column_widths header ;
List.iter ~f:compute_column_widths data ;
Array.get widths
in
let align_row, align_of_column =
let align_column i align =
let l, r = Light_table.alignment_chars align in
l ^ String.make (column_width i - 2) '-' ^ r
in
match alignments with
| Some aligns ->
let aligns_ar = Array.of_list aligns in
let aligns_get i =
if i >= Array.length aligns_ar then `Left
else Option.value ~default:`Left aligns_ar.(i)
in
([List.mapi ~f:align_column aligns], aligns_get)
| None -> ([], fun _ -> `Left)
in
let padding n = str (String.make n ' ') in
let fmt_cell i s =
let pad = column_width i - Fmt.str_length s in
let l, r =
if pad <= 0 then (noop, noop)
else
match align_of_column i with
| `Left -> (noop, padding pad)
| `Center -> (padding (pad / 2), padding ((pad + 1) / 2))
| `Right -> (padding pad, noop)
in
l $ str s $ r
in
let fmt_row row =
let row = List.mapi row ~f:fmt_cell in
str "|" $ list row (str "|") Fn.id $ str "|"
in
fmt_block_markup ~force_break:true "t"
(vbox 0 (list (header @ align_row @ data) force_break fmt_row))
and fmt_table c table =
match Light_table.of_table table with
| Some light -> fmt_table_light c light
| None -> fmt_table_heavy c table
and fmt_code_block_tag = function
| `Tag t -> ign_loc ~f:str t
| `Binding (k, v) -> ign_loc ~f:str k $ str "=" $ ign_loc ~f:str v
and fmt_code_block c ~loc (b : code_block) =
let content =
let content, _warnings =
Odoc_parser.codeblock_content loc b.content.value
in
match b.meta with
| Some {language= {value= "ocaml"; _}; _} | None -> (
(* [offset] doesn't take into account code blocks nested into lists. *)
match c.fmt_code c.conf ~offset:0 ~set_margin:true content with
| Ok formatted -> formatted |> Format_.asprintf "%a" Fmt.eval
| Error (`Msg message) ->
if
(not (String.is_empty message))
&& Option.is_some b.meta
&& not c.conf.opr_opts.quiet.v
then
Docstring.warn Stdlib.Format.err_formatter
{ location= b.content.location
; message= Format.sprintf "invalid code block: %s" message } ;
content )
| Some _ -> content
in
let delim = opt b.delimiter str in
let opening =
let meta =
opt b.meta (fun meta ->
str "@"
$ ign_loc ~f:str meta.language
$ list meta.tags noop (fun t -> char ' ' $ fmt_code_block_tag t) )
in
str "{" $ delim $ meta $ str "["
in
let output_or_closing =
match b.output with
| Some elems ->
hvbox 2
( str "]" $ delim $ str "[" $ force_break
$ fmt_nestable_block_elements c elems
$ fmt_if (not (List.is_empty elems)) (break 1000 ~-2)
$ str "]}" )
| None -> str "]" $ delim $ str "}"
in
(* The content might contain an indentation when it was not formatted. *)
hvbox 0
( opening $ force_break
$ fmt_multiline_text content
$ space_break $ output_or_closing )
and fmt_nestable_block_elements c elems =
list_block_elem c elems (fmt_nestable_block_element c)
let at = char '@'
let fmt_tag_args ?arg ?txt c tag =
at $ str tag
$ opt arg (fun x -> char ' ' $ x)
$ opt txt (function
| [] -> noop
| x -> space_break $ hovbox 0 (fmt_nestable_block_elements c x) )
let wrap_see = function
| `Url -> wrap (str "<") (str ">")
| `File -> wrap (str "'") (str "'")
| `Document -> wrap (str "\"") (str "\"")
let fmt_tag c : tag -> _ = function
| `Author s -> fmt_tag_args c "author" ~arg:(str s)
| `Version s -> fmt_tag_args c "version" ~arg:(str s)
| `See (k, sr, txt) -> fmt_tag_args c "see" ~arg:(wrap_see k (str sr)) ~txt
| `Since s -> fmt_tag_args c "since" ~arg:(str s)
| `Before (s, txt) -> fmt_tag_args c "before" ~arg:(str s) ~txt
| `Deprecated txt -> fmt_tag_args c "deprecated" ~txt
| `Param (s, txt) -> fmt_tag_args c "param" ~arg:(str s) ~txt
| `Raise (s, txt) -> fmt_tag_args c "raise" ~arg:(str s) ~txt
| `Return txt -> fmt_tag_args c "return" ~txt
| `Inline -> fmt_tag_args c "inline"
| `Open -> fmt_tag_args c "open"
| `Closed -> fmt_tag_args c "closed"
| `Hidden -> fmt_tag_args c "hidden"
| `Canonical ref -> fmt_tag_args c "canonical" ~arg:(fmt_reference ref)
| `Children_order txt -> fmt_tag_args c "children_order" ~txt
| `Toc_status txt -> fmt_tag_args c "toc_status" ~txt
| `Order_category txt -> fmt_tag_args c "order_category" ~txt
| `Short_title txt -> fmt_tag_args c "short_title" ~txt
let fmt_block_element c elm =
match elm.Loc.value with
| `Tag tag -> hovbox 2 (fmt_tag c tag)
| `Heading (lvl, lbl, elems) ->
let lvl = Int.to_string lvl in
let lbl =
match lbl with
| Some lbl -> str ":" $ str_normalized ~wrap:false lbl
| None -> noop
in
let tag = str lvl $ lbl in
hovbox 0
(fmt_markup_with_inline_elements c
~wrap:c.conf.fmt_opts.wrap_docstrings.v ~force_space:true tag
elems )
| #nestable_block_element as value ->
hovbox 0 (fmt_nestable_block_element c {elm with value})
let fmt_ast conf ~fmt_code (docs : t) =
let c = {fmt_code; conf} in
vbox 0 (list_block_elem c docs (fmt_block_element c))
let beginning_offset (conf : Conf.t) input =
let whitespace_count =
match String.indent_of_line input with Some c -> c | None -> 1
in
if conf.fmt_opts.ocp_indent_compat.v && not conf.fmt_opts.wrap_docstrings.v
then
(* Preserve offset of the first line and indent the whole comment based
on that. *)
whitespace_count
else min whitespace_count 1
let fmt_parsed (conf : Conf.t) ?trailing_space ~fmt_code ~input ~offset
parsed =
let open Fmt in
let begin_offset = beginning_offset conf input in
(* The offset is used to adjust the margin when formatting code blocks. *)
let offset = offset + begin_offset in
let fmt_code conf ~offset:offset' ~set_margin input =
fmt_code conf ~offset:(offset + offset') ~set_margin input
in
let trailing_space =
match trailing_space with
| Some sp -> sp
| None ->
fmt_if
(String.length input > 1 && String.ends_with_whitespace input)
space_break
in
let fmt_parsed parsed =
str (String.make begin_offset ' ')
$ fmt_ast conf ~fmt_code parsed
$ trailing_space
in
match parsed with
| _ when not conf.fmt_opts.parse_docstrings.v -> str input
| Ok parsed -> fmt_parsed parsed
| Error msgs ->
if not conf.opr_opts.quiet.v then
List.iter msgs ~f:(Docstring.warn Format.err_formatter) ;
str input
|