diff --git a/src/ast.ml b/src/ast.ml index 863f60c4..065c19e9 100644 --- a/src/ast.ml +++ b/src/ast.ml @@ -9,6 +9,7 @@ type 'a with_location = 'a Loc.with_location type style = [ `Bold | `Italic | `Emphasis | `Superscript | `Subscript ] +type alignment = [ `Left | `Center | `Right ] type reference_kind = [ `Simple | `With_text ] (** References in doc comments can be of two kinds: [{!simple}] or [{{!ref}With text}]. *) @@ -29,6 +30,11 @@ type inline_element = text. Similarly the [`Link] constructor has the link itself as first parameter and the second is the replacement text. *) +type 'a cell = 'a with_location list * [ `Header | `Data ] +type 'a row = 'a cell list +type 'a grid = 'a row list +type 'a abstract_table = 'a grid * alignment option list option + type nestable_block_element = [ `Paragraph of inline_element with_location list | `Code_block of @@ -41,6 +47,7 @@ type nestable_block_element = [ `Unordered | `Ordered ] * [ `Light | `Heavy ] * nestable_block_element with_location list list + | `Table of table | `Math_block of string (** @since 2.0.0 *) ] (** Some block elements may be nested within lists or tags, but not all. The [`List] constructor has a parameter of type [\[`Light | `Heavy\]]. @@ -48,6 +55,8 @@ type nestable_block_element = {{:https://ocaml.org/releases/4.12/htmlman/ocamldoc.html#sss:ocamldoc-list}manual}). *) +and table = nestable_block_element abstract_table * [ `Light | `Heavy ] + type internal_tag = [ `Canonical of string with_location | `Inline | `Open | `Closed ] (** Internal tags are used to exercise fine control over the output of odoc. They diff --git a/src/compat.ml b/src/compat.ml new file mode 100644 index 00000000..a7b535d1 --- /dev/null +++ b/src/compat.ml @@ -0,0 +1,32 @@ +module Option = struct + type 'a t = 'a option = None | Some of 'a + + let is_some = function None -> false | Some _ -> true + let value ~default = function None -> default | Some x -> x + + let join_list l = + let rec loop acc = function + | [] -> Some (List.rev acc) + | Some a :: q -> loop (a :: acc) q + | None :: _ -> None + in + loop [] l +end + +module Char = struct + include Char + + let equal (x : char) y = x = y +end + +module String = struct + include String + + let for_all f str = + let rec aux i = + if i >= String.length str then true + else if f (String.get str i) then aux (i + 1) + else false + in + aux 0 +end diff --git a/src/compat.mli b/src/compat.mli new file mode 100644 index 00000000..0959145c --- /dev/null +++ b/src/compat.mli @@ -0,0 +1,26 @@ +(** @since 4.08 *) +module Option : sig + type 'a t = 'a option = None | Some of 'a + + val is_some : 'a option -> bool + (** [is_some o] is [true] if and only if [o] is [Some o]. *) + + val value : default:'a -> 'a option -> 'a + val join_list : 'a option list -> 'a list option +end + +module Char : sig + include module type of Char + + val equal : t -> t -> bool + (** The equal function for chars. + @since 4.03.0 *) +end + +module String : sig + include module type of String + + val for_all : (char -> bool) -> string -> bool + (** [for_all p s] checks if all characters in [s] satisfy the preficate [p]. + @since 4.13.0 *) +end diff --git a/src/lexer.mll b/src/lexer.mll index d8eac89c..9c42ed29 100644 --- a/src/lexer.mll +++ b/src/lexer.mll @@ -244,7 +244,7 @@ let heading_level input level = let markup_char = - ['{' '}' '[' ']' '@'] + ['{' '}' '[' ']' '@' '|'] let space_char = [' ' '\t' '\n' '\r'] let bullet_char = @@ -289,6 +289,9 @@ rule token input = parse | (horizontal_space* (newline horizontal_space*)? as p) '}' { emit input `Right_brace ~adjust_start_by:p } + | '|' + { emit input `Bar } + | word_char (word_char | bullet_char | '@')* | bullet_char (word_char | bullet_char | '@')+ as w { emit input (`Word (unescape_word w)) } @@ -398,6 +401,21 @@ rule token input = parse | "{-" { emit input (`Begin_list_item `Dash) } + | "{table" + { emit input (`Begin_table_heavy) } + + | "{t" + { emit input (`Begin_table_light) } + + | "{tr" + { emit input `Begin_table_row } + + | "{th" + { emit input (`Begin_table_cell `Header) } + + | "{td" + { emit input (`Begin_table_cell `Data) } + | '{' (['0'-'9']+ as level) ':' (([^ '}'] # space_char)* as label) { emit input (`Begin_section_heading (heading_level input level, Some label)) } diff --git a/src/loc.ml b/src/loc.ml index e3f5a07c..0316fa27 100644 --- a/src/loc.ml +++ b/src/loc.ml @@ -22,3 +22,11 @@ let span spans = let nudge_start offset span = { span with start = { span.start with column = span.start.column + offset } } + +let spans_multiple_lines = function + | { + location = + { start = { line = start_line; _ }; end_ = { line = end_line; _ }; _ }; + _; + } -> + end_line > start_line diff --git a/src/loc.mli b/src/loc.mli index 0afe7355..135ba035 100644 --- a/src/loc.mli +++ b/src/loc.mli @@ -39,3 +39,7 @@ val map : ('a -> 'b) -> 'a with_location -> 'b with_location val same : _ with_location -> 'b -> 'b with_location (** [same x y] retuns the value y wrapped in a {!with_location} whose location is that of [x] *) + +val spans_multiple_lines : _ with_location -> bool +(** [spans_multiple_lines x] checks to see whether [x] is located + on a single line or whether it covers more than one. *) diff --git a/src/syntax.ml b/src/syntax.ml index 347ebf44..48be287f 100644 --- a/src/syntax.ml +++ b/src/syntax.ml @@ -17,6 +17,8 @@ sequence of block elements, so [block_element_list] is the top-level parser. It is also used for list item and tag content. *) +open! Compat + type 'a with_location = 'a Loc.with_location (* {2 Input} *) @@ -36,6 +38,106 @@ let peek input = | Some token -> token | None -> assert false +module Table = struct + module Light_syntax = struct + let valid_align = function + | [ { Loc.value = `Word w; _ } ] -> ( + match String.length w with + | 0 -> `Valid None + | 1 -> ( + match w with + | "-" -> `Valid None + | ":" -> `Valid (Some `Center) + | _ -> `Invalid) + | len -> + if String.for_all (Char.equal '-') (String.sub w 1 (len - 2)) then + match (String.get w 0, String.get w (len - 1)) with + | ':', ':' -> `Valid (Some `Center) + | ':', '-' -> `Valid (Some `Left) + | '-', ':' -> `Valid (Some `Right) + | '-', '-' -> `Valid None + | _ -> `Invalid + else `Invalid) + | _ -> `Invalid + + let valid_align_row lx = + let rec loop acc = function + | [] -> Some (List.rev acc) + | x :: q -> ( + match valid_align x with + | `Invalid -> None + | `Valid alignment -> loop (alignment :: acc) q) + in + loop [] lx + + let create ~grid ~align : Ast.table = + let to_block x = Loc.at x.Loc.location (`Paragraph [ x ]) in + let cell_to_block (x, k) = (List.map to_block x, k) in + let row_to_block = List.map cell_to_block in + let grid_to_block = List.map row_to_block in + ((grid_to_block grid, align), `Light) + + let with_kind kind : 'a with_location list list -> 'a Ast.row = + List.map (fun c -> (c, kind)) + + let from_raw_data grid : Ast.table = + match grid with + | [] -> create ~grid:[] ~align:None + | row1 :: rows2_N -> ( + match valid_align_row row1 with + (* If the first line is the align row, everything else is data. *) + | Some _ as align -> + create ~grid:(List.map (with_kind `Data) rows2_N) ~align + | None -> ( + match rows2_N with + (* Only 1 line, if this is not the align row this is data. *) + | [] -> create ~grid:[ with_kind `Data row1 ] ~align:None + | row2 :: rows3_N -> ( + match valid_align_row row2 with + (* If the second line is the align row, the first one is the + header and the rest is data. *) + | Some _ as align -> + let header = with_kind `Header row1 in + let data = List.map (with_kind `Data) rows3_N in + create ~grid:(header :: data) ~align + (* No align row in the first 2 lines, everything is considered + data. *) + | None -> + create ~grid:(List.map (with_kind `Data) grid) ~align:None + ))) + end + + module Heavy_syntax = struct + let create ~grid : Ast.table = ((grid, None), `Heavy) + let from_grid grid : Ast.table = create ~grid + end +end + +module Reader = struct + let until_rbrace input acc = + let rec consume () = + let next_token = peek input in + match next_token.value with + | `Right_brace -> + junk input; + `End (acc, next_token.location) + | `Space _ | `Single_newline _ | `Blank_line _ -> + junk input; + consume () + | _ -> `Token next_token + in + consume () + + module Infix = struct + let ( >>> ) consume if_token = + match consume with + | `End (ret, loc) -> (ret, loc) + | `Token t -> if_token t + end +end + +open Reader.Infix + (* The last token in the stream is always [`End], and it is never consumed by the parser, so the [None] case is impossible. *) @@ -99,6 +201,9 @@ let rec inline_element : | `Plus -> junk input; Loc.at location (`Word "+") + | `Bar -> + junk input; + Loc.at location (`Word "|") | (`Code_span _ | `Math_span _ | `Raw_markup _) as token -> junk input; Loc.at location token @@ -249,6 +354,9 @@ and delimited_inline_element_list : junk input; let element = Loc.same next_token (`Space ws) in consume_elements ~at_start_of_line:true (element :: acc) + | `Bar as token -> + let acc = inline_element input next_token.location token :: acc in + consume_elements ~at_start_of_line:false acc | (`Minus | `Plus) as bullet -> (if at_start_of_line then let suggestion = @@ -340,8 +448,8 @@ let paragraph : input -> Ast.nestable_block_element with_location = fun acc -> let next_token = peek input in match next_token.value with - | (`Space _ | `Minus | `Plus | #token_that_always_begins_an_inline_element) - as token -> + | ( `Space _ | `Minus | `Plus | `Bar + | #token_that_always_begins_an_inline_element ) as token -> let element = inline_element input next_token.location token in paragraph_line (element :: acc) | _ -> acc @@ -354,7 +462,7 @@ let paragraph : input -> Ast.nestable_block_element with_location = fun acc -> match npeek 2 input with | { value = `Single_newline ws; location } - :: { value = #token_that_always_begins_an_inline_element; _ } + :: { value = #token_that_always_begins_an_inline_element | `Bar; _ } :: _ -> junk input; let acc = Loc.at location (`Space ws) :: acc in @@ -371,7 +479,7 @@ let paragraph : input -> Ast.nestable_block_element with_location = (* {3 Helper types} *) (* The interpretation of tokens in the block parser depends on where on a line - each token appears. The five possible "locations" are: + each token appears. The six possible "locations" are: - [`At_start_of_line], when only whitespace has been read on the current line. @@ -381,6 +489,7 @@ let paragraph : input -> Ast.nestable_block_element with_location = [-], has been read, and only whitespace has been read since. - [`After_explicit_list_bullet], when a valid explicit bullet, such as [{li], has been read, and only whitespace has been read since. + - [`After_table_cell], when a table cell opening markup ('{th' or '{td') has been read. - [`After_text], when any other valid non-whitespace token has already been read on the current line. @@ -404,6 +513,7 @@ type where_in_line = | `After_tag | `After_shorthand_bullet | `After_explicit_list_bullet + | `After_table_cell | `After_text ] (* The block parsing loop, function [block_element_list], stops when it @@ -457,6 +567,7 @@ type ('block, 'stops_at_which_tokens) context = | Top_level : (Ast.block_element, stops_at_delimiters) context | In_shorthand_list : (Ast.nestable_block_element, stopped_implicitly) context | In_explicit_list : (Ast.nestable_block_element, stops_at_delimiters) context + | In_table_cell : (Ast.nestable_block_element, stops_at_delimiters) context | In_tag : (Ast.nestable_block_element, Token.t) context (* This is a no-op. It is needed to prove to the type system that nestable block @@ -471,6 +582,7 @@ let accepted_in_all_contexts : | Top_level -> (block :> Ast.block_element) | In_shorthand_list -> block | In_explicit_list -> block + | In_table_cell -> block | In_tag -> block (* Converts a tag to a series of words. This is used in error recovery, when a @@ -499,6 +611,7 @@ let tag_to_words = function - paragraphs, - code blocks, - verbatim text blocks, + - tables, - lists, and - section headings. *) let rec block_element_list : @@ -561,6 +674,7 @@ let rec block_element_list : | Top_level -> (List.rev acc, next_token, where_in_line) | In_shorthand_list -> (List.rev acc, next_token, where_in_line) | In_explicit_list -> (List.rev acc, next_token, where_in_line) + | In_table_cell -> (List.rev acc, next_token, where_in_line) | In_tag -> (List.rev acc, next_token, where_in_line)) (* Whitespace. This can terminate some kinds of block elements. It is also necessary to track it to interpret [`Minus] and [`Plus] correctly, as @@ -594,6 +708,32 @@ let rec block_element_list : ~suggestion location |> add_warning input; + junk input; + consume_block_elements ~parsed_a_tag where_in_line acc + (* Table rows ([{tr ...}]) can never appear directly + in block content. They can only appear inside [{table ...}]. *) + | { value = `Begin_table_row as token; location } -> + let suggestion = + Printf.sprintf "move %s into %s." (Token.print token) + (Token.describe `Begin_table_heavy) + in + Parse_error.not_allowed ~what:(Token.describe token) + ~in_what:(Token.describe parent_markup) + ~suggestion location + |> add_warning input; + junk input; + consume_block_elements ~parsed_a_tag where_in_line acc + (* Table cells ([{th ...}] and [{td ...}]) can never appear directly + in block content. They can only appear inside [{tr ...}]. *) + | { value = `Begin_table_cell _ as token; location } -> + let suggestion = + Printf.sprintf "move %s into %s." (Token.print token) + (Token.describe `Begin_table_row) + in + Parse_error.not_allowed ~what:(Token.describe token) + ~in_what:(Token.describe parent_markup) + ~suggestion location + |> add_warning input; junk input; consume_block_elements ~parsed_a_tag where_in_line acc (* Tags. These can appear at the top level only. Also, once one tag is seen, @@ -622,6 +762,7 @@ let rec block_element_list : if where_in_line = `At_start_of_line then (List.rev acc, next_token, where_in_line) else recover_when_not_at_top_level context + | In_table_cell -> recover_when_not_at_top_level context | In_tag -> if where_in_line = `At_start_of_line then (List.rev acc, next_token, where_in_line) @@ -706,8 +847,8 @@ let rec block_element_list : let tag = Loc.at location (`Tag tag) in consume_block_elements ~parsed_a_tag:true `After_text (tag :: acc))) - | { value = #token_that_always_begins_an_inline_element; _ } as next_token - -> + | ( { value = #token_that_always_begins_an_inline_element; _ } + | { value = `Bar; _ } ) as next_token -> warn_if_after_tags next_token; warn_if_after_text next_token; @@ -800,6 +941,25 @@ let rec block_element_list : let block = Loc.at location block in let acc = block :: acc in consume_block_elements ~parsed_a_tag `After_text acc + | { value = (`Begin_table_light | `Begin_table_heavy) as token; location } + as next_token -> + warn_if_after_tags next_token; + warn_if_after_text next_token; + junk input; + let block, brace_location = + let parent_markup = token in + let parent_markup_location = location in + match token with + | `Begin_table_light -> + light_table input ~parent_markup ~parent_markup_location + | `Begin_table_heavy -> + heavy_table input ~parent_markup ~parent_markup_location + in + let location = Loc.span [ location; brace_location ] in + let block = accepted_in_all_contexts context (`Table block) in + let block = Loc.at location block in + let acc = block :: acc in + consume_block_elements ~parsed_a_tag `After_text acc | { value = (`Minus | `Plus) as token; location } as next_token -> ( (match where_in_line with | `After_text | `After_shorthand_bullet -> @@ -855,6 +1015,7 @@ let rec block_element_list : (List.rev acc, next_token, where_in_line) else recover_when_not_at_top_level context | In_explicit_list -> recover_when_not_at_top_level context + | In_table_cell -> recover_when_not_at_top_level context | In_tag -> recover_when_not_at_top_level context | Top_level -> if where_in_line <> `At_start_of_line then @@ -914,6 +1075,7 @@ let rec block_element_list : | Top_level -> `At_start_of_line | In_shorthand_list -> `After_shorthand_bullet | In_explicit_list -> `After_explicit_list_bullet + | In_table_cell -> `After_table_cell | In_tag -> `After_tag in @@ -1067,6 +1229,135 @@ and explicit_list_items : consume_list_items [] +(* Consumes a sequence of table rows that might start with [`Bar]. + + This function is called immediately after '{t' ([`Begin_table `Light]) is + read. The only "valid" way to exit is by reading a [`Right_brace] token, + which is consumed. *) +and light_table ~parent_markup ~parent_markup_location input = + let rec consume_rows acc ~last_loc = + Reader.until_rbrace input acc >>> fun next_token -> + match next_token.Loc.value with + | `Bar | #token_that_always_begins_an_inline_element -> ( + let next, row, last_loc = + light_table_row ~parent_markup ~last_loc input + in + match next with + | `Continue -> consume_rows (row :: acc) ~last_loc + | `Stop -> (row :: acc, last_loc)) + | other_token -> + Parse_error.not_allowed next_token.location + ~what:(Token.describe other_token) + ~in_what:(Token.describe parent_markup) + |> add_warning input; + junk input; + consume_rows acc ~last_loc + in + let rows, brace_location = consume_rows [] ~last_loc:parent_markup_location in + let grid = List.rev rows in + (Table.Light_syntax.from_raw_data grid, brace_location) + +(* Consumes a table row that might start with [`Bar]. *) +and light_table_row ~parent_markup ~last_loc input = + let rec consume_row acc_row acc_cell ~new_line ~last_loc = + let push_cells row cell = + match cell with [] -> row | _ -> List.rev cell :: row + in + let return row cell = List.rev (push_cells row cell) in + let next_token = peek input in + match next_token.value with + | `Right_brace -> + junk input; + (`Stop, return acc_row acc_cell, next_token.location) + | `Space _ -> + junk input; + consume_row acc_row acc_cell ~new_line ~last_loc + | `Single_newline _ | `Blank_line _ -> + junk input; + (`Continue, return acc_row acc_cell, last_loc) + | `Bar -> + junk input; + let acc_row = if new_line then [] else List.rev acc_cell :: acc_row in + consume_row acc_row [] ~new_line:false ~last_loc + | #token_that_always_begins_an_inline_element as token -> + let i = inline_element input next_token.location token in + if Loc.spans_multiple_lines i then + Parse_error.not_allowed + ~what:(Token.describe (`Single_newline "")) + ~in_what:(Token.describe `Begin_table_light) + i.location + |> add_warning input; + consume_row acc_row (i :: acc_cell) ~new_line:false + ~last_loc:next_token.location + | other_token -> + Parse_error.not_allowed next_token.location + ~what:(Token.describe other_token) + ~in_what:(Token.describe parent_markup) + |> add_warning input; + junk input; + consume_row acc_row acc_cell ~new_line ~last_loc + in + consume_row [] [] ~new_line:true ~last_loc + +(* Consumes a sequence of table rows (starting with '{tr ...}', which are + represented by [`Begin_table_row] tokens). + + This function is called immediately after '{table' ([`Begin_table `Heavy]) is + read. The only "valid" way to exit is by reading a [`Right_brace] token, + which is consumed. *) +and heavy_table ~parent_markup ~parent_markup_location input = + let rec consume_rows acc ~last_loc = + Reader.until_rbrace input acc >>> fun next_token -> + match next_token.Loc.value with + | `Begin_table_row as token -> + junk input; + let items, last_loc = heavy_table_row ~parent_markup:token input in + consume_rows (List.rev items :: acc) ~last_loc + | token -> + Parse_error.not_allowed next_token.location ~what:(Token.describe token) + ~in_what:(Token.describe parent_markup) + ~suggestion:"Move outside of {table ...}, or inside {tr ...}" + |> add_warning input; + junk input; + consume_rows acc ~last_loc + in + let rows, brace_location = consume_rows [] ~last_loc:parent_markup_location in + let grid = List.rev rows in + (Table.Heavy_syntax.from_grid grid, brace_location) + +(* Consumes a sequence of table cells (starting with '{th ...}' or '{td ... }', + which are represented by [`Begin_table_cell] tokens). + + This function is called immediately after '{tr' ([`Begin_table_row]) is + read. The only "valid" way to exit is by reading a [`Right_brace] token, + which is consumed. *) +and heavy_table_row ~parent_markup input = + let rec consume_cell_items acc = + Reader.until_rbrace input acc >>> fun next_token -> + match next_token.Loc.value with + | `Begin_table_cell kind as token -> + junk input; + let content, token_after_list_item, _where_in_line = + block_element_list In_table_cell ~parent_markup:token input + in + (match token_after_list_item.value with + | `Right_brace -> junk input + | `End -> + Parse_error.not_allowed token_after_list_item.location + ~what:(Token.describe `End) ~in_what:(Token.describe token) + |> add_warning input); + consume_cell_items ((content, kind) :: acc) + | token -> + Parse_error.not_allowed next_token.location ~what:(Token.describe token) + ~in_what:(Token.describe parent_markup) + ~suggestion: + "Move outside of {table ...}, or inside {td ...} or {th ...}" + |> add_warning input; + junk input; + consume_cell_items acc + in + consume_cell_items [] + (* {2 Entry point} *) let parse warnings tokens = diff --git a/src/token.ml b/src/token.ml index 222820f0..c76b3112 100644 --- a/src/token.ml +++ b/src/token.ml @@ -69,8 +69,14 @@ type t = | (* List markup. *) `Begin_list of [ `Unordered | `Ordered ] | `Begin_list_item of [ `Li | `Dash ] + | (* Table markup. *) + `Begin_table_light + | `Begin_table_heavy + | `Begin_table_row + | `Begin_table_cell of [ `Header | `Data ] | `Minus | `Plus + | `Bar | section_heading | tag ] @@ -87,8 +93,14 @@ let print : [< t ] -> string = function | `Begin_link_with_replacement_text _ -> "'{{:'" | `Begin_list_item `Li -> "'{li ...}'" | `Begin_list_item `Dash -> "'{- ...}'" + | `Begin_table_light -> "{t" + | `Begin_table_heavy -> "{table" + | `Begin_table_row -> "'{tr'" + | `Begin_table_cell `Header -> "'{th'" + | `Begin_table_cell `Data -> "'{td'" | `Minus -> "'-'" | `Plus -> "'+'" + | `Bar -> "'|'" | `Begin_section_heading (level, label) -> let label = match label with None -> "" | Some label -> ":" ^ label in Printf.sprintf "'{%i%s'" level label @@ -142,8 +154,14 @@ let describe : [< t | `Comment ] -> string = function | `Begin_list `Ordered -> "'{ol ...}' (numbered list)" | `Begin_list_item `Li -> "'{li ...}' (list item)" | `Begin_list_item `Dash -> "'{- ...}' (list item)" + | `Begin_table_light -> "'{t ...}' (table)" + | `Begin_table_heavy -> "'{table ...}' (table)" + | `Begin_table_row -> "'{tr ...}' (table row)" + | `Begin_table_cell `Header -> "'{th ... }' (table header cell)" + | `Begin_table_cell `Data -> "'{td ... }' (table data cell)" | `Minus -> "'-' (bulleted list item)" | `Plus -> "'+' (numbered list item)" + | `Bar -> "'|'" | `Begin_section_heading (level, _) -> Printf.sprintf "'{%i ...}' (section heading)" level | `Tag (`Author _) -> "'@author'" diff --git a/test/test.ml b/test/test.ml index c91c83c0..4fedc471 100644 --- a/test/test.ml +++ b/test/test.ml @@ -29,6 +29,12 @@ module Ast_to_sexp = struct | `Superscript -> Atom "superscript" | `Subscript -> Atom "subscript" + let alignment : Ast.alignment option -> sexp = function + | Some `Left -> Atom "left" + | Some `Center -> Atom "center" + | Some `Right -> Atom "right" + | None -> Atom "default" + let reference_kind : Ast.reference_kind -> sexp = function | `Simple -> Atom "simple" | `With_text -> Atom "with_text" @@ -80,6 +86,24 @@ module Ast_to_sexp = struct |> fun items -> List items in List [ Atom kind; Atom weight; items ] + | `Table ((grid, align), s) -> + let syntax = function `Light -> "light" | `Heavy -> "heavy" in + let kind = function `Header -> "header" | `Data -> "data" in + let map name x f = List [ Atom name; List (List.map f x) ] in + let alignment = + match align with + | None -> List [ Atom "align"; Atom "no alignment" ] + | Some align -> map "align" align @@ alignment + in + List + [ + Atom "table"; + List [ Atom "syntax"; Atom (syntax s) ]; + ( map "grid" grid @@ fun row -> + map "row" row @@ fun (cell, k) -> + map (kind k) cell @@ at.at (nestable_block_element at) ); + alignment; + ] let tag at : Ast.tag -> sexp = function | `Author s -> List [ Atom "@author"; Atom s ] @@ -387,7 +411,7 @@ let%expect_test _ = () let%expect_test _ = - let module Plus_minus_words = struct + let module Plus_minus_bar_words = struct let minus_in_word = test "foo-bar"; [%expect @@ -426,6 +450,36 @@ let%expect_test _ = ((f.ml (1 4) (1 5)) (word +))))))) (warnings ())) |}] + let bar_in_word = + test "foo|bar"; + [%expect + {| + ((output + (((f.ml (1 0) (1 7)) + (paragraph + (((f.ml (1 0) (1 3)) (word foo)) ((f.ml (1 3) (1 4)) (word |)) + ((f.ml (1 4) (1 7)) (word bar))))))) + (warnings ())) |}] + + let escaped_bar_in_word = + test "foo\\|bar"; + [%expect + {| + ((output + (((f.ml (1 0) (1 8)) (paragraph (((f.ml (1 0) (1 8)) (word "foo\\|bar"))))))) + (warnings ())) |}] + + let bar_as_word = + test "foo |"; + [%expect + {| + ((output + (((f.ml (1 0) (1 5)) + (paragraph + (((f.ml (1 0) (1 3)) (word foo)) ((f.ml (1 3) (1 4)) space) + ((f.ml (1 4) (1 5)) (word |))))))) + (warnings ())) |}] + let negative_number = test "-3.14 -1337"; [%expect diff --git a/test/test.mli b/test/test.mli new file mode 100644 index 00000000..4f83ff60 --- /dev/null +++ b/test/test.mli @@ -0,0 +1 @@ +val test : ?location:Odoc_parser.Loc.point -> string -> unit diff --git a/test/test_tables.ml b/test/test_tables.ml new file mode 100644 index 00000000..2084e9b9 --- /dev/null +++ b/test/test_tables.ml @@ -0,0 +1,755 @@ +open Test + +[@@@ocaml.warning "-32"] + +let%expect_test _ = + let module Heavy = struct + let empty_table_heavy = + test "{table }"; + [%expect + {| + ((output + (((f.ml (1 0) (1 8)) + (table (syntax heavy) (grid ()) (align "no alignment"))))) + (warnings ())) |}] + + let empty_row = + test "{table {tr } }"; + [%expect + {| + ((output + (((f.ml (1 0) (1 14)) + (table (syntax heavy) (grid ((row ()))) (align "no alignment"))))) + (warnings ()))|}] + + let no_header = + test "{table {tr {td}}}"; + [%expect + {| + ((output + (((f.ml (1 0) (1 17)) + (table (syntax heavy) (grid ((row ((data ()))))) (align "no alignment"))))) + (warnings ())) |}] + + let no_data = + test "{table {tr {th}}}"; + [%expect + {| + ((output + (((f.ml (1 0) (1 17)) + (table (syntax heavy) (grid ((row ((header ()))))) + (align "no alignment"))))) + (warnings ())) |}] + + let bad_data = + test "{table absurd content}"; + [%expect + {| + ((output + (((f.ml (1 0) (1 22)) + (table (syntax heavy) (grid ()) (align "no alignment"))))) + (warnings + ( "File \"f.ml\", line 1, characters 7-13:\ + \n'absurd' is not allowed in '{table ...}' (table).\ + \nSuggestion: Move outside of {table ...}, or inside {tr ...}" + "File \"f.ml\", line 1, characters 14-21:\ + \n'content' is not allowed in '{table ...}' (table).\ + \nSuggestion: Move outside of {table ...}, or inside {tr ...}"))) |}] + + let bad_row = + test "{table {tr absurd content}}"; + [%expect + {| + ((output + (((f.ml (1 0) (1 27)) + (table (syntax heavy) (grid ((row ()))) (align "no alignment"))))) + (warnings + ( "File \"f.ml\", line 1, characters 11-17:\ + \n'absurd' is not allowed in '{tr ...}' (table row).\ + \nSuggestion: Move outside of {table ...}, or inside {td ...} or {th ...}" + "File \"f.ml\", line 1, characters 18-25:\ + \n'content' is not allowed in '{tr ...}' (table row).\ + \nSuggestion: Move outside of {table ...}, or inside {td ...} or {th ...}"))) |}] + + let multiple_headers = + test "{table {tr {th}} {tr {th}} {tr {td}}}"; + [%expect + {| + ((output + (((f.ml (1 0) (1 37)) + (table (syntax heavy) + (grid ((row ((header ()))) (row ((header ()))) (row ((data ()))))) + (align "no alignment"))))) + (warnings ())) |}] + + let complex_table = + test + {| + {table + {tr + {th xxx} + {th yyy} + } + {tr + {td aaaa bbb ccc {i ddd} + } + {td + {table {tr {td}}} + } + } + {tr + {td + - aaa + - bbb + - ccc + } + {td + {t + x | y | z + --|---|-- + 1 | 2 | 3 + } + } + } + } + |}; + [%expect + {| + ((output + (((f.ml (2 8) (28 9)) + (table (syntax heavy) + (grid + ((row + ((header + (((f.ml (4 16) (4 19)) + (paragraph (((f.ml (4 16) (4 19)) (word xxx))))))) + (header + (((f.ml (5 16) (5 19)) + (paragraph (((f.ml (5 16) (5 19)) (word yyy))))))))) + (row + ((data + (((f.ml (8 16) (8 36)) + (paragraph + (((f.ml (8 16) (8 20)) (word aaaa)) ((f.ml (8 20) (8 21)) space) + ((f.ml (8 21) (8 24)) (word bbb)) ((f.ml (8 24) (8 25)) space) + ((f.ml (8 25) (8 28)) (word ccc)) ((f.ml (8 28) (8 29)) space) + ((f.ml (8 29) (8 36)) + (italic (((f.ml (8 32) (8 35)) (word ddd)))))))))) + (data + (((f.ml (11 15) (11 32)) + (table (syntax heavy) (grid ((row ((data ()))))) + (align "no alignment"))))))) + (row + ((data + (((f.ml (16 15) (18 20)) + (unordered light + ((((f.ml (16 17) (16 20)) + (paragraph (((f.ml (16 17) (16 20)) (word aaa)))))) + (((f.ml (17 17) (17 20)) + (paragraph (((f.ml (17 17) (17 20)) (word bbb)))))) + (((f.ml (18 17) (18 20)) + (paragraph (((f.ml (18 17) (18 20)) (word ccc))))))))))) + (data + (((f.ml (21 14) (25 15)) + (table (syntax light) + (grid + ((row + ((header + (((f.ml (22 17) (22 18)) + (paragraph (((f.ml (22 17) (22 18)) (word x))))))) + (header + (((f.ml (22 21) (22 22)) + (paragraph (((f.ml (22 21) (22 22)) (word y))))))) + (header + (((f.ml (22 25) (22 26)) + (paragraph (((f.ml (22 25) (22 26)) (word z))))))))) + (row + ((data + (((f.ml (24 17) (24 18)) + (paragraph (((f.ml (24 17) (24 18)) (word 1))))))) + (data + (((f.ml (24 21) (24 22)) + (paragraph (((f.ml (24 21) (24 22)) (word 2))))))) + (data + (((f.ml (24 25) (24 26)) + (paragraph (((f.ml (24 25) (24 26)) (word 3))))))))))) + (align (default default default)))))))))) + (align "no alignment"))))) + (warnings ())) |}] + end in + () + +let%expect_test _ = + let module Light = struct + let empty_table_light = + test "{t }"; + [%expect + {| + ((output + (((f.ml (1 0) (1 4)) + (table (syntax light) (grid ()) (align "no alignment"))))) + (warnings ())) |}] + + let simple = + test {| + {t + | a | + } + |}; + [%expect + {| + ((output + (((f.ml (2 8) (4 9)) + (table (syntax light) + (grid + ((row + ((data + (((f.ml (3 12) (3 13)) + (paragraph (((f.ml (3 12) (3 13)) (word a))))))))))) + (align "no alignment"))))) + (warnings ())) |}] + + let stars = + test + {| + {t + |a| *b*| + |*c| d* | + } + |}; + [%expect + {| + ((output + (((f.ml (2 8) (5 9)) + (table (syntax light) + (grid + ((row + ((data + (((f.ml (3 11) (3 12)) + (paragraph (((f.ml (3 11) (3 12)) (word a))))))) + (data + (((f.ml (3 16) (3 19)) + (paragraph (((f.ml (3 16) (3 19)) (word *b*))))))))) + (row + ((data + (((f.ml (4 11) (4 13)) + (paragraph (((f.ml (4 11) (4 13)) (word *c))))))) + (data + (((f.ml (4 15) (4 17)) + (paragraph (((f.ml (4 15) (4 17)) (word d*))))))))))) + (align "no alignment"))))) + (warnings ())) |}] + + let backquotes = + test {| + {t + | `a |` + } + |}; + [%expect + {| + ((output + (((f.ml (2 6) (4 7)) + (table (syntax light) + (grid + ((row + ((data + (((f.ml (3 11) (3 13)) + (paragraph (((f.ml (3 11) (3 13)) (word `a))))))) + (data + (((f.ml (3 15) (3 16)) + (paragraph (((f.ml (3 15) (3 16)) (word `))))))))))) + (align "no alignment"))))) + (warnings ())) |}] + + let no_header = + test {| + {t + |---|---| + | x | y | + } + |}; + [%expect + {| + ((output + (((f.ml (2 6) (5 7)) + (table (syntax light) + (grid + ((row + ((data + (((f.ml (4 9) (4 10)) (paragraph (((f.ml (4 9) (4 10)) (word x))))))) + (data + (((f.ml (4 13) (4 14)) + (paragraph (((f.ml (4 13) (4 14)) (word y))))))))))) + (align (default default)))))) + (warnings ())) |}] + + let no_align = + test {| + {t + | x | y | + | x | y | + } + |}; + [%expect + {| + ((output + (((f.ml (2 6) (5 7)) + (table (syntax light) + (grid + ((row + ((data + (((f.ml (3 9) (3 10)) (paragraph (((f.ml (3 9) (3 10)) (word x))))))) + (data + (((f.ml (3 13) (3 14)) + (paragraph (((f.ml (3 13) (3 14)) (word y))))))))) + (row + ((data + (((f.ml (4 9) (4 10)) (paragraph (((f.ml (4 9) (4 10)) (word x))))))) + (data + (((f.ml (4 13) (4 14)) + (paragraph (((f.ml (4 13) (4 14)) (word y))))))))))) + (align "no alignment"))))) + (warnings ())) |}] + + let only_align = + test {| + {t + |--|--| + } + |}; + [%expect + {| + ((output + (((f.ml (2 6) (4 7)) + (table (syntax light) (grid ()) (align (default default)))))) + (warnings ())) |}] + + let no_data = + test {| + {t + | x | y | + |---|---| + } + |}; + [%expect + {| + ((output + (((f.ml (2 6) (5 7)) + (table (syntax light) + (grid + ((row + ((header + (((f.ml (3 9) (3 10)) (paragraph (((f.ml (3 9) (3 10)) (word x))))))) + (header + (((f.ml (3 13) (3 14)) + (paragraph (((f.ml (3 13) (3 14)) (word y))))))))))) + (align (default default)))))) + (warnings ())) |}] + + let alignment = + test + {| + {t + | a | b | c | d | + |---|:--|--:|:-:| + } + |}; + [%expect + {| + ((output + (((f.ml (2 6) (5 7)) + (table (syntax light) + (grid + ((row + ((header + (((f.ml (3 9) (3 10)) (paragraph (((f.ml (3 9) (3 10)) (word a))))))) + (header + (((f.ml (3 13) (3 14)) + (paragraph (((f.ml (3 13) (3 14)) (word b))))))) + (header + (((f.ml (3 17) (3 18)) + (paragraph (((f.ml (3 17) (3 18)) (word c))))))) + (header + (((f.ml (3 21) (3 22)) + (paragraph (((f.ml (3 21) (3 22)) (word d))))))))))) + (align (default left right center)))))) + (warnings ())) |}] + + let no_bars = + test + {| + {t + a | b | c | d + ---|:--|--:|:-: + a | b | c | d + } + |}; + [%expect + {| + ((output + (((f.ml (2 6) (6 7)) + (table (syntax light) + (grid + ((row + ((header + (((f.ml (3 8) (3 9)) (paragraph (((f.ml (3 8) (3 9)) (word a))))))) + (header + (((f.ml (3 12) (3 13)) + (paragraph (((f.ml (3 12) (3 13)) (word b))))))) + (header + (((f.ml (3 16) (3 17)) + (paragraph (((f.ml (3 16) (3 17)) (word c))))))) + (header + (((f.ml (3 20) (3 21)) + (paragraph (((f.ml (3 20) (3 21)) (word d))))))))) + (row + ((data + (((f.ml (5 8) (5 9)) (paragraph (((f.ml (5 8) (5 9)) (word a))))))) + (data + (((f.ml (5 12) (5 13)) + (paragraph (((f.ml (5 12) (5 13)) (word b))))))) + (data + (((f.ml (5 16) (5 17)) + (paragraph (((f.ml (5 16) (5 17)) (word c))))))) + (data + (((f.ml (5 20) (5 21)) + (paragraph (((f.ml (5 20) (5 21)) (word d))))))))))) + (align (default left right center)))))) + (warnings ())) |}] + + let light_table_new_lines = + test + {| + {t + + | a | b | c | d | + + |---|---|---|---| + + | a | b | c | d | + + } + |}; + [%expect + {| + ((output + (((f.ml (2 6) (10 7)) + (table (syntax light) + (grid + ((row + ((header + (((f.ml (4 9) (4 10)) (paragraph (((f.ml (4 9) (4 10)) (word a))))))) + (header + (((f.ml (4 13) (4 14)) + (paragraph (((f.ml (4 13) (4 14)) (word b))))))) + (header + (((f.ml (4 17) (4 18)) + (paragraph (((f.ml (4 17) (4 18)) (word c))))))) + (header + (((f.ml (4 21) (4 22)) + (paragraph (((f.ml (4 21) (4 22)) (word d))))))))) + (row + ((data + (((f.ml (8 9) (8 10)) (paragraph (((f.ml (8 9) (8 10)) (word a))))))) + (data + (((f.ml (8 13) (8 14)) + (paragraph (((f.ml (8 13) (8 14)) (word b))))))) + (data + (((f.ml (8 17) (8 18)) + (paragraph (((f.ml (8 17) (8 18)) (word c))))))) + (data + (((f.ml (8 21) (8 22)) + (paragraph (((f.ml (8 21) (8 22)) (word d))))))))))) + (align (default default default default)))))) + (warnings ())) |}] + + let light_table_markup = + test + {| + {t + | {i a} {:google.com} \t | | {m b} {e c} {% xyz %} | {b d} [foo] | + |---|---|---|---| + } + |}; + [%expect + {| + ((output + (((f.ml (2 6) (5 7)) + (table (syntax light) + (grid + ((row + ((header + (((f.ml (3 9) (3 14)) + (paragraph + (((f.ml (3 9) (3 14)) + (italic (((f.ml (3 12) (3 13)) (word a)))))))) + ((f.ml (3 15) (3 28)) + (paragraph (((f.ml (3 15) (3 28)) (google.com ()))))) + ((f.ml (3 29) (3 31)) + (paragraph (((f.ml (3 29) (3 31)) (word "\\t"))))))) + (header ()) + (header + (((f.ml (3 36) (3 41)) + (paragraph (((f.ml (3 36) (3 41)) (math_span b))))) + ((f.ml (3 42) (3 47)) + (paragraph + (((f.ml (3 42) (3 47)) + (emphasis (((f.ml (3 45) (3 46)) (word c)))))))) + ((f.ml (3 48) (3 57)) + (paragraph (((f.ml (3 48) (3 57)) (raw_markup () " xyz "))))))) + (header + (((f.ml (3 60) (3 65)) + (paragraph + (((f.ml (3 60) (3 65)) (bold (((f.ml (3 63) (3 64)) (word d)))))))) + ((f.ml (3 66) (3 71)) + (paragraph (((f.ml (3 66) (3 71)) (code_span foo))))))))))) + (align (default default default default)))))) + (warnings ())) |}] + + let light_table_markup_with_newlines = + test + {| + {t | h1 | h2 | + |--------------|-------------| + | {e with + newlines} | {b d} [foo] | + } + |}; + [%expect + {| + ((output + (((f.ml (2 6) (6 7)) + (table (syntax light) + (grid + ((row + ((header + (((f.ml (2 11) (2 13)) + (paragraph (((f.ml (2 11) (2 13)) (word h1))))))) + (header + (((f.ml (2 26) (2 28)) + (paragraph (((f.ml (2 26) (2 28)) (word h2))))))))) + (row + ((data + (((f.ml (4 11) (5 23)) + (paragraph + (((f.ml (4 11) (5 23)) + (emphasis + (((f.ml (4 14) (4 18)) (word with)) + ((f.ml (4 18) (5 14)) space) + ((f.ml (5 14) (5 22)) (word newlines)))))))))) + (data + (((f.ml (5 26) (5 31)) + (paragraph + (((f.ml (5 26) (5 31)) (bold (((f.ml (5 29) (5 30)) (word d)))))))) + ((f.ml (5 32) (5 37)) + (paragraph (((f.ml (5 32) (5 37)) (code_span foo))))))))))) + (align (default default)))))) + (warnings + ( "File \"f.ml\", line 4, character 11 to line 5, character 23:\ + \nLine break is not allowed in '{t ...}' (table)."))) |}] + + let no_space = + test + {| + {t + | a | b |c| d | + |---|--:|:--|:-:| + } + |}; + [%expect + {| + ((output + (((f.ml (2 7) (5 8)) + (table (syntax light) + (grid + ((row + ((header + (((f.ml (3 11) (3 12)) + (paragraph (((f.ml (3 11) (3 12)) (word a))))))) + (header + (((f.ml (3 15) (3 16)) + (paragraph (((f.ml (3 15) (3 16)) (word b))))))) + (header + (((f.ml (3 18) (3 19)) + (paragraph (((f.ml (3 18) (3 19)) (word c))))))) + (header + (((f.ml (3 21) (3 22)) + (paragraph (((f.ml (3 21) (3 22)) (word d))))))))))) + (align (default right left center)))))) + (warnings ())) |}] + + let multiple_headers = + test + {| + {t + ||a|b| + |:-|---:| + |c|d| + |cc|dd| + |-:|:-:| + |e|f| + |g|h|| + } + |}; + [%expect + {| + ((output + (((f.ml (2 6) (10 7)) + (table (syntax light) + (grid + ((row + ((header ()) + (header + (((f.ml (3 9) (3 10)) (paragraph (((f.ml (3 9) (3 10)) (word a))))))) + (header + (((f.ml (3 11) (3 12)) + (paragraph (((f.ml (3 11) (3 12)) (word b))))))))) + (row + ((data + (((f.ml (5 8) (5 9)) (paragraph (((f.ml (5 8) (5 9)) (word c))))))) + (data + (((f.ml (5 10) (5 11)) + (paragraph (((f.ml (5 10) (5 11)) (word d))))))))) + (row + ((data + (((f.ml (6 8) (6 10)) + (paragraph (((f.ml (6 8) (6 10)) (word cc))))))) + (data + (((f.ml (6 11) (6 13)) + (paragraph (((f.ml (6 11) (6 13)) (word dd))))))))) + (row + ((data + (((f.ml (7 8) (7 10)) + (paragraph (((f.ml (7 8) (7 10)) (word -:))))))) + (data + (((f.ml (7 11) (7 14)) + (paragraph (((f.ml (7 11) (7 14)) (word :-:))))))))) + (row + ((data + (((f.ml (8 8) (8 9)) (paragraph (((f.ml (8 8) (8 9)) (word e))))))) + (data + (((f.ml (8 10) (8 11)) + (paragraph (((f.ml (8 10) (8 11)) (word f))))))))) + (row + ((data + (((f.ml (9 8) (9 9)) (paragraph (((f.ml (9 8) (9 9)) (word g))))))) + (data + (((f.ml (9 10) (9 11)) + (paragraph (((f.ml (9 10) (9 11)) (word h))))))) + (data ()))))) + (align (left right)))))) + (warnings ())) |}] + + let block_element_in_cell = + test + {| + {t + | {[ a ]} | b | + |---|---| + } + |}; + [%expect + {| + ((output + (((f.ml (2 11) (5 12)) + (table (syntax light) + (grid + ((row + ((header ()) + (header + (((f.ml (3 23) (3 24)) + (paragraph (((f.ml (3 23) (3 24)) (word b))))))))))) + (align (default default)))))) + (warnings + ( "File \"f.ml\", line 3, characters 13-20:\ + \n'{[...]}' (code block) is not allowed in '{t ...}' (table)."))) |}] + + let block_element_in_row = + test + {| + {t + {[ a ]} + | a | b | + |---|---| + } + |}; + [%expect + {| + ((output + (((f.ml (2 11) (6 12)) + (table (syntax light) + (grid + ((row + ((header + (((f.ml (4 13) (4 14)) + (paragraph (((f.ml (4 13) (4 14)) (word a))))))) + (header + (((f.ml (4 17) (4 18)) + (paragraph (((f.ml (4 17) (4 18)) (word b))))))))))) + (align (default default)))))) + (warnings + ( "File \"f.ml\", line 3, characters 11-18:\ + \n'{[...]}' (code block) is not allowed in '{t ...}' (table)."))) |}] + + let more_cells_later = + test + {| + {t + | x | y | + |---|---| + | x | y | z | + } + |}; + [%expect + {| + ((output + (((f.ml (2 6) (6 7)) + (table (syntax light) + (grid + ((row + ((header + (((f.ml (3 9) (3 10)) (paragraph (((f.ml (3 9) (3 10)) (word x))))))) + (header + (((f.ml (3 13) (3 14)) + (paragraph (((f.ml (3 13) (3 14)) (word y))))))))) + (row + ((data + (((f.ml (5 9) (5 10)) (paragraph (((f.ml (5 9) (5 10)) (word x))))))) + (data + (((f.ml (5 13) (5 14)) + (paragraph (((f.ml (5 13) (5 14)) (word y))))))) + (data + (((f.ml (5 17) (5 18)) + (paragraph (((f.ml (5 17) (5 18)) (word z))))))))))) + (align (default default)))))) + (warnings ())) |}] + + let less_cells_later = + test + {| + {t + | x | y | + |---|---| + x + } + |}; + [%expect + {| + ((output + (((f.ml (2 6) (6 7)) + (table (syntax light) + (grid + ((row + ((header + (((f.ml (3 9) (3 10)) (paragraph (((f.ml (3 9) (3 10)) (word x))))))) + (header + (((f.ml (3 13) (3 14)) + (paragraph (((f.ml (3 13) (3 14)) (word y))))))))) + (row + ((data + (((f.ml (5 7) (5 8)) (paragraph (((f.ml (5 7) (5 8)) (word x))))))))))) + (align (default default)))))) + (warnings ())) |}] + end in + ()