cli11, bsd manual
This commit is contained in:
parent
9c7c528a0c
commit
18c97eacd0
7 changed files with 534 additions and 117 deletions
209
lib/manpage.ml
209
lib/manpage.ml
|
|
@ -435,33 +435,25 @@ let count_macro name lines =
|
|||
|
||||
(* Auto-detect and try strategies, return the one with most entries *)
|
||||
let extract_entries lines =
|
||||
let results = ref [] in
|
||||
(* Try TP if .TP macros present *)
|
||||
if count_macro "TP" lines > 0 then
|
||||
results := ("TP", strategy_tp lines) :: !results;
|
||||
(* Try IP if .IP macros present *)
|
||||
if count_macro "IP" lines > 0 then
|
||||
results := ("IP", strategy_ip lines) :: !results;
|
||||
(* Try PP+RS if both present *)
|
||||
if count_macro "PP" lines > 0 && count_macro "RS" lines > 0 then
|
||||
results := ("PP+RS", strategy_pp_rs lines) :: !results;
|
||||
(* Try nix3 style if UR macros present *)
|
||||
if count_macro "UR" lines > 0 && count_macro "IP" lines > 0 then
|
||||
results := ("nix", strategy_nix lines) :: !results;
|
||||
(* Always try deroff as fallback *)
|
||||
results := ("deroff", strategy_deroff_lines lines) :: !results;
|
||||
(* Prefer specialized strategies over deroff fallback *)
|
||||
let specialized =
|
||||
List.filter (fun (name, entries) -> name <> "deroff" && entries <> []) !results
|
||||
let tp = count_macro "TP" lines
|
||||
and ip = count_macro "IP" lines
|
||||
and pp = count_macro "PP" lines
|
||||
and rs = count_macro "RS" lines
|
||||
and ur = count_macro "UR" lines in
|
||||
let specialized = List.filter_map Fun.id [
|
||||
(if tp > 0 then Some ("TP", strategy_tp lines) else None);
|
||||
(if ip > 0 then Some ("IP", strategy_ip lines) else None);
|
||||
(if pp > 0 && rs > 0 then Some ("PP+RS", strategy_pp_rs lines) else None);
|
||||
(if ur > 0 && ip > 0 then Some ("nix", strategy_nix lines) else None);
|
||||
] in
|
||||
let candidates = match List.filter (fun (_, e) -> e <> []) specialized with
|
||||
| [] -> [("deroff", strategy_deroff_lines lines)]
|
||||
| filtered -> filtered
|
||||
in
|
||||
let candidates = if specialized <> [] then specialized else !results in
|
||||
let best =
|
||||
List.fold_left (fun (best_name, best_entries) (name, entries) ->
|
||||
if List.length entries >= List.length best_entries then (name, entries)
|
||||
else (best_name, best_entries)
|
||||
) ("none", []) candidates
|
||||
in
|
||||
snd best
|
||||
List.fold_left (fun (_, best) (name, entries) ->
|
||||
if List.length entries >= List.length best then (name, entries)
|
||||
else (name, best)
|
||||
) ("none", []) candidates |> snd
|
||||
|
||||
(* --- SYNOPSIS command name extraction --- *)
|
||||
|
||||
|
|
@ -514,12 +506,171 @@ let extract_synopsis_command contents =
|
|||
let lines = String.split_on_char '\n' contents in
|
||||
extract_synopsis_command_lines lines
|
||||
|
||||
(* --- SYNOPSIS positional extraction --- *)
|
||||
|
||||
let extract_synopsis_positionals_lines lines =
|
||||
let classified = List.map classify_line lines in
|
||||
let is_synopsis name =
|
||||
String.uppercase_ascii (String.trim name) = "SYNOPSIS"
|
||||
in
|
||||
let rec find = function
|
||||
| [] -> []
|
||||
| Macro ("SH", args) :: rest when is_synopsis args -> collect rest []
|
||||
| _ :: rest -> find rest
|
||||
and collect lines acc =
|
||||
match lines with
|
||||
| [] -> finish acc
|
||||
| Macro ("SH", _) :: _ -> finish acc
|
||||
| Macro ("SS", _) :: _ -> finish acc
|
||||
| Macro ("br", _) :: _ -> finish acc
|
||||
| Text s :: rest ->
|
||||
let s = strip_groff_escapes s |> String.trim in
|
||||
collect rest (if String.length s > 0 then s :: acc else acc)
|
||||
| Macro (("B" | "BI" | "BR" | "I" | "IR" | "IB" | "RB" | "RI"), args) :: rest ->
|
||||
let s = strip_inline_macro_args args |> strip_groff_escapes |> String.trim in
|
||||
collect rest (if String.length s > 0 then s :: acc else acc)
|
||||
| _ :: rest -> collect rest acc
|
||||
and finish acc =
|
||||
let parts = List.rev acc in
|
||||
let full = String.concat " " parts |> String.trim in
|
||||
if String.length full = 0 then []
|
||||
else
|
||||
let cmd_end = skip_command_prefix full in
|
||||
let args = String.sub full cmd_end (String.length full - cmd_end) in
|
||||
parse_usage_args args
|
||||
in
|
||||
find classified
|
||||
|
||||
(* --- mdoc (BSD) format support --- *)
|
||||
|
||||
let is_mdoc lines =
|
||||
List.exists (fun l ->
|
||||
match classify_line l with Macro ("Sh", _) -> true | _ -> false
|
||||
) lines
|
||||
|
||||
let mdoc_text_of line =
|
||||
match line with
|
||||
| Text s -> Some (strip_groff_escapes s)
|
||||
| Macro (m, args) ->
|
||||
(match m with
|
||||
| "Pp" | "Bl" | "El" | "Sh" | "Ss" | "Os" | "Dd" | "Dt"
|
||||
| "Oo" | "Oc" | "Op" -> None
|
||||
| _ ->
|
||||
let s = strip_groff_escapes args |> String.trim in
|
||||
if s = "" then None else Some s)
|
||||
| _ -> None
|
||||
|
||||
let parse_mdoc_lines lines =
|
||||
let classified = List.map classify_line lines in
|
||||
let entries = ref [] in
|
||||
let positionals = ref [] in
|
||||
let rec skip_to_el = function
|
||||
| [] -> []
|
||||
| Macro ("El", _) :: rest -> rest
|
||||
| _ :: rest -> skip_to_el rest
|
||||
in
|
||||
let collect_desc rest =
|
||||
let rec go acc = function
|
||||
| [] -> (acc, [])
|
||||
| (Macro ("It", _) | Macro ("El", _)
|
||||
| Macro ("Sh", _) | Macro ("Ss", _)) :: _ as rest -> (acc, rest)
|
||||
| line :: rest ->
|
||||
go (match mdoc_text_of line with Some s -> s :: acc | None -> acc) rest
|
||||
in
|
||||
let parts, rest = go [] rest in
|
||||
(String.concat " " (List.rev parts) |> String.trim, rest)
|
||||
in
|
||||
let parse_mdoc_it args =
|
||||
let words = String.split_on_char ' ' args
|
||||
|> List.filter (fun w -> w <> "" && w <> "Ns") in
|
||||
match words with
|
||||
| "Fl" :: c :: _ when String.length c = 1 && is_alphanumeric c.[0] ->
|
||||
let param = match words with
|
||||
| _ :: _ :: "Ar" :: p :: _ -> Some (Mandatory p)
|
||||
| _ -> None
|
||||
in
|
||||
Some { switch = Short c.[0]; param; desc = "" }
|
||||
| "Fl" :: name :: _ when String.length name > 1 && name.[0] = '-' ->
|
||||
let long = String.sub name 1 (String.length name - 1) in
|
||||
let param = match words with
|
||||
| _ :: _ :: "Ar" :: p :: _ -> Some (Mandatory p)
|
||||
| _ -> None
|
||||
in
|
||||
Some { switch = Long long; param; desc = "" }
|
||||
| _ -> None
|
||||
in
|
||||
let rec parse_option_list = function
|
||||
| [] -> []
|
||||
| Macro ("El", _) :: rest -> rest
|
||||
| Macro ("It", args) :: rest ->
|
||||
let desc, rest = collect_desc rest in
|
||||
(match parse_mdoc_it args with
|
||||
| Some e -> entries := { e with desc } :: !entries
|
||||
| None -> ());
|
||||
parse_option_list rest
|
||||
| _ :: rest -> parse_option_list rest
|
||||
in
|
||||
let rec scan = function
|
||||
| [] -> ()
|
||||
| Macro ("Bl", _) :: Macro ("It", it_args) :: rest ->
|
||||
let words = String.split_on_char ' ' it_args
|
||||
|> List.filter (fun w -> w <> "") in
|
||||
if (match words with "Fl" :: _ -> true | _ -> false) then begin
|
||||
let desc, rest = collect_desc rest in
|
||||
(match parse_mdoc_it it_args with
|
||||
| Some e -> entries := { e with desc } :: !entries
|
||||
| None -> ());
|
||||
scan (parse_option_list rest)
|
||||
end else
|
||||
scan (skip_to_el rest)
|
||||
| Macro ("Bl", _) :: rest -> scan (skip_to_el rest)
|
||||
| Macro ("Sh", args) :: rest
|
||||
when String.uppercase_ascii (String.trim args) = "SYNOPSIS" ->
|
||||
scan (parse_synopsis rest)
|
||||
| _ :: rest -> scan rest
|
||||
and parse_synopsis = function
|
||||
| [] -> []
|
||||
| Macro ("Sh", _) :: _ as rest -> rest
|
||||
| Macro ("Ar", args) :: rest ->
|
||||
let words = String.split_on_char ' ' args
|
||||
|> List.filter (fun w -> w <> "") in
|
||||
(match words with
|
||||
| name :: _ when String.length name >= 2 ->
|
||||
let variadic = List.mem "..." words in
|
||||
positionals := { pos_name = String.lowercase_ascii name;
|
||||
optional = false; variadic } :: !positionals
|
||||
| _ -> ());
|
||||
parse_synopsis rest
|
||||
| Macro ("Op", args) :: rest ->
|
||||
let words = String.split_on_char ' ' args
|
||||
|> List.filter (fun w -> w <> "") in
|
||||
(match words with
|
||||
| "Ar" :: name :: _ when String.length name >= 2 ->
|
||||
let variadic = List.mem "..." words in
|
||||
positionals := { pos_name = String.lowercase_ascii name;
|
||||
optional = true; variadic } :: !positionals
|
||||
| _ -> ());
|
||||
parse_synopsis rest
|
||||
| _ :: rest -> parse_synopsis rest
|
||||
in
|
||||
scan classified;
|
||||
let seen = Hashtbl.create 8 in
|
||||
let positionals = List.rev !positionals |> List.filter (fun p ->
|
||||
if Hashtbl.mem seen p.pos_name then false
|
||||
else (Hashtbl.replace seen p.pos_name true; true)) in
|
||||
{ entries = List.rev !entries; subcommands = []; positionals }
|
||||
|
||||
(* --- Top-level API --- *)
|
||||
|
||||
let parse_manpage_lines lines =
|
||||
let options_section = extract_options_section lines in
|
||||
let entries = extract_entries options_section in
|
||||
{ entries; subcommands = [] }
|
||||
if is_mdoc lines then
|
||||
parse_mdoc_lines lines
|
||||
else begin
|
||||
let options_section = extract_options_section lines in
|
||||
let entries = extract_entries options_section in
|
||||
let positionals = extract_synopsis_positionals_lines lines in
|
||||
{ entries; subcommands = []; positionals }
|
||||
end
|
||||
|
||||
let parse_manpage_string contents =
|
||||
let lines = String.split_on_char '\n' contents in
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue