├── .gitignore ├── .ocamlformat ├── CHANGES.md ├── LICENSE.md ├── README.md ├── bin ├── dune └── main.ml ├── dune-project ├── lib ├── dune ├── pp_markdown.ml ├── pp_markdown.mli ├── toc.ml └── toc.mli ├── test ├── dune └── toc.ml ├── toc.opam └── toc.opam.locked /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | _coverage 3 | _metrics 4 | *~ 5 | *.install 6 | *.merlin 7 | _opam 8 | .envrc 9 | \#* 10 | .#* 11 | .*.swp 12 | **/.DS_Store 13 | -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | version = 0.24.1 2 | profile = conventional 3 | 4 | break-infix = fit-or-vertical 5 | parse-docstrings = true 6 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 2 | 3 | - Initial release 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## ISC License 2 | 3 | Copyright (c) 2022 Thomas Gazagnaire 4 | 5 | Permission to use, copy, modify, and distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TOC - Manage Tables of Contents in Markdown Files 2 | 3 | This repository contains a simple utility to manage tables of contents for 4 | Github Markdown files. 5 | 6 |
7 | 8 | * [Usage](#Usage) 9 | * [First Use](#First-Use) 10 | * [Regenerate TOC](#Regenerate-TOC) 11 | * [Multiple TOC](#Multiple-TOC) 12 | * [Options](#Options) 13 | * [Print](#Print) 14 | * [Depth](#Depth) 15 | * [License](#License) 16 | 17 |
18 | 19 | ## Usage 20 | 21 | ### First Use 22 | 23 | Run: 24 | 25 | ``` 26 | $ toc README.md 27 | README.md has been updated. 28 | ``` 29 | 30 | This will replace any `[toc]` tokens by the file's table of contents instead. 31 | 32 | ```diff 33 | -[toc] 34 | +
35 | + 36 | +* [Usage](#Usage) 37 | + * [First Use](#First-Use) 38 | + * [Regenerate TOC](#Regenerate-TOC) 39 | + * [Multiple TOC](#Multiple-TOC) 40 | + * [Options](#Options) 41 | + * [Print](#Print) 42 | + * [Depth](#Depth) 43 | +* [License](#License) 44 | + 45 | +
46 | ``` 47 | 48 | ### Regenerate TOC 49 | 50 | `toc` adds transparent `
` markers around the generated table of 51 | contents. Hence, the next call to `toc` will update the table 52 | correctly. 53 | 54 | To regenerate the TOC, run: 55 | 56 | ``` 57 | $ toc README.md 58 | README.md has been updated. 59 | ``` 60 | 61 | ### Multiple TOC 62 | 63 | Use as many `[toc]` as you like to define multiple table of contents 64 | the same file. 65 | 66 | ### Options 67 | 68 | Use `toc --help` to see all the options. 69 | 70 | #### Print 71 | 72 | ``` 73 | $ toc README.md -p 74 | * Usage 75 | * First Use 76 | * Regenerate TOC 77 | * Multiple TOC 78 | * Options 79 | * Print 80 | * Depth 81 | * License 82 | ``` 83 | 84 | #### Depth 85 | 86 | Use `--depth` to control the depth of the table of contents. 87 | 88 | ``` 89 | $ toc README.md -p -d 1 90 | * Usage 91 | * License 92 | ``` 93 | 94 | ## License 95 | 96 | ISC. See the [LICENSE file](./LICENSE.md). 97 | -------------------------------------------------------------------------------- /bin/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (public_name toc) 3 | (name main) 4 | (libraries omd toc cmdliner logs.cli logs.fmt fmt.tty fmt.cli)) 5 | -------------------------------------------------------------------------------- /bin/main.ml: -------------------------------------------------------------------------------- 1 | (* 2 | * Copyright (c) 2022 Thomas Gazagnaire 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | *) 16 | 17 | let main () file depth print = 18 | let ic = open_in file in 19 | let md = Omd.of_channel ic in 20 | match print with 21 | | true -> 22 | let toc = Toc.v ~add_links:false ?depth md in 23 | Fmt.pr "%a%!" Toc.pp toc; 24 | close_in ic 25 | | false -> ( 26 | let doc = Toc.expand ?depth md in 27 | close_in ic; 28 | match doc with 29 | | None -> Fmt.pr "No changes.\n%!" 30 | | Some doc -> 31 | let oc = open_out file in 32 | output_string oc (Toc.to_string doc); 33 | close_out oc; 34 | Fmt.pr "%s has been updated.\n%!" file) 35 | 36 | let setup style_renderer level = 37 | Fmt_tty.setup_std_outputs ?style_renderer (); 38 | Logs.set_level level; 39 | Logs.set_reporter (Logs_fmt.reporter ()); 40 | () 41 | 42 | open Cmdliner 43 | 44 | let input_file = 45 | let doc = Arg.info ~doc:"Markdown file to expand." ~docv:"FILE" [] in 46 | Arg.(required @@ pos 0 (some file) None doc) 47 | 48 | let depth = 49 | let doc = Arg.info ~doc:"The table of contents' depth." [ "depth"; "d" ] in 50 | Arg.(value @@ opt (some int) None doc) 51 | 52 | let print = 53 | let doc = 54 | Arg.info ~doc:"Print the table of contents and exit." [ "print"; "p" ] 55 | in 56 | Arg.(value @@ flag doc) 57 | 58 | let setup_log = 59 | Term.(const setup $ Fmt_cli.style_renderer () $ Logs_cli.level ()) 60 | 61 | let () = 62 | let man = [ `S "DESCRIPTION"; `P "TODO" ] in 63 | let info = 64 | Cmd.info "toc" ~man 65 | ~doc:"Replace [toc] annotations in Markdown files with actual contents." 66 | in 67 | let cmd = 68 | Cmd.v info Term.(const main $ setup_log $ input_file $ depth $ print) 69 | in 70 | exit (Cmd.eval cmd) 71 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.5) 2 | (name toc) 3 | 4 | (generate_opam_files true) 5 | 6 | (source (github samoht/toc)) 7 | (license ISC) 8 | (authors "Thomas Gazagnaire") 9 | (maintainers "Thomas Gazagnaire") 10 | 11 | (package 12 | (name toc) 13 | (synopsis "A generator of table of contents for Github Markdown files") 14 | (depends 15 | (alcotest :with-test) 16 | ocaml 17 | dune 18 | (omd (>= 2.0.0~alpha2)) 19 | (cmdliner (>= 1.1.0)) 20 | logs 21 | fmt 22 | astring)) 23 | -------------------------------------------------------------------------------- /lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (public_name toc) 3 | (libraries omd fmt logs astring)) 4 | -------------------------------------------------------------------------------- /lib/pp_markdown.ml: -------------------------------------------------------------------------------- 1 | (* From https://github.com/ocaml/omd/pull/265 *) 2 | 3 | open Omd 4 | 5 | module Option = struct 6 | let value o ~default = match o with Some v -> v | None -> default 7 | let iter f = function Some v -> f v | None -> () 8 | let map f o = match o with None -> None | Some v -> Some (f v) 9 | end 10 | 11 | let nchar n buf c = 12 | let rec aux n = 13 | if n <= 0 then () 14 | else ( 15 | Buffer.add_char buf c; 16 | aux (n - 1)) 17 | in 18 | aux n 19 | 20 | let nl buf = Buffer.add_char buf '\n' 21 | let sp buf = Buffer.add_char buf ' ' 22 | 23 | let add_string_escape_chars b s = 24 | for i = 0 to String.length s - 1 do 25 | match s.[i] with 26 | | '<' as c -> 27 | if 28 | i <> String.length s - 1 29 | && match s.[i + 1] with 'a' .. 'z' | 'A' .. 'Z' -> false | _ -> true 30 | then Buffer.add_char b '\\'; 31 | Buffer.add_char b c 32 | | '>' as c -> 33 | if i = 0 || match s.[i - 1] with ' ' | '\n' -> false | _ -> true then 34 | Buffer.add_char b '\\'; 35 | Buffer.add_char b c 36 | | '#' as c -> 37 | if i = 0 || s.[i - 1] = '\n' then Buffer.add_char b '\\'; 38 | Buffer.add_char b c 39 | | c -> Buffer.add_char b c 40 | done 41 | 42 | let infer_num_backticks min_allowed c = 43 | let l = String.length c in 44 | let rec loop m s i = 45 | if i = l then max m s 46 | else 47 | match c.[i] with 48 | | '`' -> loop m (s + 1) (i + 1) 49 | | _ -> loop (max m s) 1 (i + 1) 50 | in 51 | loop min_allowed 1 0 52 | 53 | let add_attrs_to_buffer ?(space = false) buf attrs = 54 | let add_attr (k, v) = 55 | match k with 56 | | "class" -> 57 | String.split_on_char ' ' v 58 | |> List.map (Printf.sprintf ".%s") 59 | |> String.concat " " 60 | |> Buffer.add_string buf 61 | | "id" -> Printf.bprintf buf "#%s" v 62 | | k -> Printf.bprintf buf "%s=%s" k v 63 | in 64 | match attrs with 65 | | [] -> () 66 | | attr :: attrs -> 67 | if space then sp buf; 68 | Buffer.add_char buf '{'; 69 | add_attr attr; 70 | List.iter 71 | (fun (k, v) -> 72 | sp buf; 73 | add_attr (k, v)) 74 | attrs; 75 | Buffer.add_char buf '}' 76 | 77 | let rec inline ~prefix buf = function 78 | | Concat (_, l) -> List.iter (inline ~prefix buf) l 79 | | Text (_, t) -> add_string_escape_chars buf t 80 | | Emph (_, il) -> 81 | Buffer.add_char buf '*'; 82 | inline ~prefix buf il; 83 | Buffer.add_char buf '*' 84 | | Strong (_, il) -> 85 | Buffer.add_string buf "**"; 86 | inline ~prefix buf il; 87 | Buffer.add_string buf "**" 88 | | Code (attr, c) -> 89 | let n = String.length c in 90 | let num_backticks = infer_num_backticks 1 c in 91 | nchar num_backticks buf '`'; 92 | if n > 0 && c.[0] = '`' then sp buf; 93 | Buffer.add_string buf c; 94 | if n > 0 && c.[n - 1] = '`' then sp buf; 95 | nchar num_backticks buf '`'; 96 | add_attrs_to_buffer buf attr 97 | | Hard_break _attr -> 98 | sp buf; 99 | sp buf; 100 | nl buf; 101 | Buffer.add_string buf prefix 102 | | Soft_break _ -> 103 | nl buf; 104 | Buffer.add_string buf prefix 105 | | Html (_, body) -> Buffer.add_string buf body 106 | | Link (attr, link) -> add_link ~prefix buf attr link 107 | | Image (attr, link) -> 108 | Buffer.add_char buf '!'; 109 | add_link ~prefix buf attr link 110 | 111 | and add_link ~prefix buf attr { label; destination; title } = 112 | Buffer.add_char buf '['; 113 | inline ~prefix buf label; 114 | Buffer.add_char buf ']'; 115 | Buffer.add_char buf '('; 116 | Buffer.add_string buf destination; 117 | Option.iter (Printf.bprintf buf " %S") title; 118 | Buffer.add_char buf ')'; 119 | add_attrs_to_buffer buf attr 120 | 121 | let add_nl_with_quote_prefix ~prefix ~quote ~tight buf = 122 | if (not tight) && Buffer.length buf > 1 then ( 123 | if quote then Buffer.add_string buf prefix; 124 | nl buf) 125 | 126 | let add_prefix ~first_item_prefix ~prefix buf = 127 | let prefix = Option.value ~default:prefix first_item_prefix in 128 | Buffer.add_string buf prefix 129 | 130 | let add_string_with_nl_prefix ~first_item_prefix ~prefix buf s = 131 | String.iter 132 | (fun x -> 133 | Buffer.add_char buf x; 134 | if x = '\n' then add_prefix ~first_item_prefix ~prefix buf) 135 | s 136 | 137 | let rec block ?first_item_prefix ~prefix ~quote ~tight buf b = 138 | match b with 139 | | Blockquote (_attr, q) -> ( 140 | match q with 141 | | [] -> () 142 | | hd :: tl -> 143 | let first_item_prefix = 144 | Option.map (fun s -> s ^ "> ") first_item_prefix 145 | in 146 | let prefix = prefix ^ "> " in 147 | block ?first_item_prefix ~prefix ~quote ~tight buf hd; 148 | List.iter (block ?first_item_prefix ~prefix ~quote:true ~tight buf) tl 149 | ) 150 | | Paragraph (_attr, md) -> 151 | add_nl_with_quote_prefix ~prefix ~quote ~tight buf; 152 | add_prefix ~first_item_prefix ~prefix buf; 153 | inline ~prefix buf md; 154 | nl buf 155 | | List (_attr, ty, spacing, bl) -> ( 156 | add_nl_with_quote_prefix ~prefix ~quote ~tight buf; 157 | let tight = match spacing with Loose -> false | Tight -> true in 158 | match ty with 159 | | Ordered (x, c) -> 160 | add_ordered_list ?first_item_prefix ~prefix ~quote ~tight buf x c bl 161 | | Bullet c -> 162 | add_bullet_list ?first_item_prefix ~prefix ~quote ~tight buf c bl) 163 | | Code_block (attr, label, c) -> 164 | add_nl_with_quote_prefix ~prefix ~quote ~tight buf; 165 | add_prefix ~first_item_prefix ~prefix buf; 166 | add_code_block ~first_item_prefix ~prefix buf attr label c 167 | | Thematic_break _attr -> 168 | add_nl_with_quote_prefix ~prefix ~quote ~tight buf; 169 | add_prefix ~first_item_prefix ~prefix buf; 170 | nchar 3 buf '*'; 171 | nl buf 172 | | Html_block (_, body) -> 173 | add_nl_with_quote_prefix ~prefix ~quote ~tight buf; 174 | add_prefix ~first_item_prefix ~prefix buf; 175 | let n = String.length body in 176 | let body = 177 | if n > 0 && body.[n - 1] = '\n' then String.sub body 0 (n - 1) else body 178 | in 179 | add_string_with_nl_prefix ~first_item_prefix ~prefix buf body; 180 | nl buf 181 | | Heading (attr, level, text) -> 182 | add_nl_with_quote_prefix ~prefix ~quote ~tight buf; 183 | add_prefix ~first_item_prefix ~prefix buf; 184 | if 0 < level && level < 7 then ( 185 | nchar level buf '#'; 186 | sp buf); 187 | inline ~prefix buf text; 188 | add_attrs_to_buffer ~space:true buf attr; 189 | nl buf 190 | | Definition_list (_attr, l) -> 191 | add_nl_with_quote_prefix ~prefix ~quote ~tight buf; 192 | add_prefix ~first_item_prefix ~prefix buf; 193 | add_def_list ~prefix buf l 194 | 195 | and add_ordered_list ?first_item_prefix ~prefix ~quote ~tight buf x c = function 196 | | [] -> () 197 | | hd :: tl -> 198 | let add_list_item before_first_item_prefix i bl = 199 | let n, s = 200 | let s = Printf.sprintf "%d%c " (x + i) c in 201 | (String.length s, s) 202 | in 203 | let first_item_prefix = before_first_item_prefix ^ s in 204 | let prefix = prefix ^ String.make n ' ' in 205 | match bl with 206 | | [] -> () 207 | | hd :: tl -> 208 | block ~first_item_prefix ~prefix ~quote ~tight buf hd; 209 | List.iter (fun b -> block ~prefix ~quote ~tight buf b) tl 210 | in 211 | add_list_item (Option.value ~default:prefix first_item_prefix) 0 hd; 212 | List.iteri (fun i -> add_list_item prefix (i + 1)) tl 213 | 214 | and add_code_block ~first_item_prefix ~prefix buf attr label c = 215 | let n = infer_num_backticks 3 c in 216 | nchar n buf '`'; 217 | let label = String.trim label in 218 | Buffer.add_string buf label; 219 | add_attrs_to_buffer ~space:true buf attr; 220 | nl buf; 221 | add_prefix ~first_item_prefix ~prefix buf; 222 | add_string_with_nl_prefix ~first_item_prefix ~prefix buf c; 223 | let n' = String.length c in 224 | if n' > 0 && c.[String.length c - 1] <> '\n' then ( 225 | nl buf; 226 | add_prefix ~first_item_prefix ~prefix buf); 227 | nchar n buf '`'; 228 | nl buf 229 | 230 | and add_bullet_list ?first_item_prefix ~prefix ~quote ~tight buf c = function 231 | | [] -> () 232 | | hd :: tl -> 233 | let n, s = 234 | let s = Printf.sprintf "%c " c in 235 | (2, s) 236 | in 237 | let add_list_item before_first_item_prefix bl = 238 | let first_item_prefix = before_first_item_prefix ^ s in 239 | let prefix = prefix ^ String.make n ' ' in 240 | match bl with 241 | | [] -> () 242 | | hd :: tl -> 243 | block ~first_item_prefix ~prefix ~quote ~tight buf hd; 244 | List.iter (fun b -> block ~prefix ~quote ~tight buf b) tl 245 | in 246 | add_list_item (Option.value ~default:prefix first_item_prefix) hd; 247 | List.iter (add_list_item prefix) tl 248 | 249 | and add_def_list ~prefix buf l = 250 | let add_term { term; defs } = 251 | inline ~prefix buf term; 252 | nl buf; 253 | List.iter 254 | (fun def -> 255 | Buffer.add_char buf ':'; 256 | sp buf; 257 | inline ~prefix buf def; 258 | nl buf) 259 | defs 260 | in 261 | match l with 262 | | [] -> () 263 | | term :: terms -> 264 | add_term term; 265 | List.iter 266 | (fun term -> 267 | nl buf; 268 | add_term term) 269 | terms 270 | 271 | let to_string (t : doc) = 272 | let buf = Buffer.create 1024 in 273 | List.iter (block ~prefix:"" ~quote:false ~tight:false buf) t; 274 | Buffer.contents buf 275 | -------------------------------------------------------------------------------- /lib/pp_markdown.mli: -------------------------------------------------------------------------------- 1 | (* From https://github.com/ocaml/omd/pull/265 *) 2 | 3 | val to_string : Omd.doc -> string 4 | -------------------------------------------------------------------------------- /lib/toc.ml: -------------------------------------------------------------------------------- 1 | (* 2 | * Copyright (c) 2022 Thomas Gazagnaire 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | *) 16 | 17 | open Omd 18 | open Astring 19 | 20 | let src = Logs.Src.create "irmin.tree" ~doc:"Persistent lazy trees for Irmin" 21 | 22 | module Log = (val Logs.src_log src : Logs.LOG) 23 | 24 | let pp = Fmt.of_to_string Omd.to_sexp 25 | 26 | type token = Toc | Begin | End 27 | 28 | let text t = Text ([], t) 29 | let html t = Html_block ([], t) 30 | let concat ts = Concat ([], List.map text ts) 31 | let toc = concat [ "["; "toc"; "]" ] 32 | let begin_toc = "
" 33 | let end_toc = "
" 34 | 35 | (* [toc] is either: 36 | - empty; in that case it appears as [toc] in the Markdown file 37 | - expanded: in that case it appears between `[//]: # begin toc` and 38 | `[//]: # end toc` markers *) 39 | let is_toc = function 40 | | Paragraph (_, x) when x = toc -> Some Toc 41 | | Html_block (_, x) when x = begin_toc -> Some Begin 42 | | Html_block (_, x) when x = end_toc -> Some End 43 | | Html_block (_, x) when x = begin_toc ^ "\n" -> Some Begin 44 | | Html_block (_, x) when x = end_toc ^ "\n" -> Some End 45 | | _ -> None 46 | 47 | let rec replace ~toc : doc -> doc = function 48 | | [] -> [] 49 | | h :: t -> ( 50 | match is_toc h with 51 | | None -> h :: replace ~toc t 52 | | Some Toc -> html begin_toc :: toc :: html end_toc :: replace ~toc t 53 | | Some Begin -> h :: toc :: skip_to_end ~toc t 54 | | Some End -> h :: replace ~toc t) 55 | 56 | and skip_to_end ~toc : doc -> doc = function 57 | | [] -> [] 58 | | h :: t -> ( 59 | match is_toc h with 60 | | Some End -> h :: replace ~toc t 61 | | _ -> skip_to_end ~toc t) 62 | 63 | module Linkify = struct 64 | (* Convert section title to a valid HTML ID. *) 65 | let title_to_id s = 66 | String.filter (fun c -> Char.Ascii.is_alphanum c || c = ' ') s 67 | |> String.map (function ' ' -> '-' | c -> c) 68 | 69 | let inline : 'attr inline -> 'attr inline = 70 | fun label -> 71 | let id = Pp_markdown.to_string [ Paragraph ([], label) ] in 72 | Link ([], { label; destination = "#" ^ title_to_id id; title = None }) 73 | 74 | let rec block : 'attr block -> 'attr block = function 75 | | Paragraph (attr, x) -> Paragraph (attr, inline x) 76 | | List (attr, ty, sp, bl) -> 77 | List (attr, ty, sp, List.map (List.map block) bl) 78 | | _ -> failwith "invalid mardkown in TOC" 79 | end 80 | 81 | type t = attributes block option 82 | 83 | let v ?(depth = 10) ?(add_links = true) doc : t = 84 | match Omd.toc ~depth ~start:[ 1 ] doc with 85 | | [] -> None 86 | | [ toc ] -> 87 | let toc = if add_links then Linkify.block toc else toc in 88 | Some toc 89 | | _ -> assert false (* this is an invariant in Omd.toc *) 90 | 91 | let expand ?depth doc = 92 | match v ?depth doc with 93 | | None -> None 94 | | Some toc -> 95 | Log.info (fun l -> l "TOC=%a" pp [ toc ]); 96 | Log.debug (fun l -> l "BEFORE: %a" pp doc); 97 | let doc = replace ~toc doc in 98 | Log.debug (fun l -> l "AFTER: %a" pp doc); 99 | Some doc 100 | 101 | let to_string = Pp_markdown.to_string 102 | 103 | let pp ppf (t : t) = 104 | match t with 105 | | None -> Fmt.pf ppf "No sections." 106 | | Some t -> Fmt.of_to_string to_string ppf [ t ] 107 | -------------------------------------------------------------------------------- /lib/toc.mli: -------------------------------------------------------------------------------- 1 | (* 2 | * Copyright (c) 2022 Thomas Gazagnaire 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | *) 16 | 17 | (** TOC library. *) 18 | 19 | val expand : ?depth:int -> Omd.doc -> Omd.doc option 20 | (** Replace [toc] in Markdown files by the result of [Omd.toc] *) 21 | 22 | val to_string : Omd.doc -> string 23 | (** TODO: use upstream [Omd.to_markdown] *) 24 | 25 | type t 26 | (** The type for table of contents. *) 27 | 28 | val v : ?depth:int -> ?add_links:bool -> Omd.doc -> t 29 | (** [v md] is [md]'s table of contents. *) 30 | 31 | val pp : t Fmt.t 32 | (** [pp] is the pretty-printer for table of contents. *) 33 | -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (test 2 | (name toc) 3 | (libraries toc alcotest)) 4 | -------------------------------------------------------------------------------- /test/toc.ml: -------------------------------------------------------------------------------- 1 | (* 2 | * Copyright (c) 2022 Thomas Gazagnaire 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | *) 16 | 17 | let () = Alcotest.run "toc" [] 18 | -------------------------------------------------------------------------------- /toc.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "A generator of table of contents for Github Markdown files" 4 | maintainer: ["Thomas Gazagnaire"] 5 | authors: ["Thomas Gazagnaire"] 6 | license: "ISC" 7 | homepage: "https://github.com/samoht/toc" 8 | bug-reports: "https://github.com/samoht/toc/issues" 9 | depends: [ 10 | "alcotest" {with-test} 11 | "ocaml" 12 | "dune" {>= "3.5"} 13 | "omd" {>= "2.0.0~alpha2"} 14 | "cmdliner" {>= "1.1.0"} 15 | "logs" 16 | "fmt" 17 | "astring" 18 | "odoc" {with-doc} 19 | ] 20 | build: [ 21 | ["dune" "subst"] {dev} 22 | [ 23 | "dune" 24 | "build" 25 | "-p" 26 | name 27 | "-j" 28 | jobs 29 | "@install" 30 | "@runtest" {with-test} 31 | "@doc" {with-doc} 32 | ] 33 | ] 34 | dev-repo: "git+https://github.com/samoht/toc.git" 35 | -------------------------------------------------------------------------------- /toc.opam.locked: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | synopsis: "opam-monorepo generated lockfile" 3 | maintainer: "opam-monorepo" 4 | depends: [ 5 | "alcotest" {= "1.6.0" & ?vendor} 6 | "astring" {= "0.8.5+dune" & ?vendor} 7 | "base-bigarray" {= "base"} 8 | "base-bytes" {= "base+dune" & ?vendor} 9 | "base-threads" {= "base"} 10 | "base-unix" {= "base"} 11 | "cmdliner" {= "1.1.1+dune" & ?vendor} 12 | "cppo" {= "1.6.9" & ?vendor} 13 | "csexp" {= "1.5.1" & ?vendor} 14 | "dune" {= "3.5.0"} 15 | "dune-configurator" {= "3.5.0" & ?vendor} 16 | "fmt" {= "0.9.0+dune" & ?vendor} 17 | "logs" {= "0.7.0+dune2" & ?vendor} 18 | "lwt" {= "5.6.1" & ?vendor} 19 | "ocaml" {= "4.14.0"} 20 | "ocaml-base-compiler" {= "4.14.0"} 21 | "ocaml-config" {= "2"} 22 | "ocaml-options-vanilla" {= "1"} 23 | "ocaml-syntax-shims" {= "1.0.0" & ?vendor} 24 | "ocplib-endian" {= "1.2" & ?vendor} 25 | "omd" {= "2.0.0~alpha2" & ?vendor} 26 | "re" {= "1.10.4" & ?vendor} 27 | "seq" {= "base+dune" & ?vendor} 28 | "stdlib-shims" {= "0.3.0" & ?vendor} 29 | "uutf" {= "1.0.3+dune" & ?vendor} 30 | ] 31 | pin-depends: [ 32 | [ 33 | "alcotest.1.6.0" 34 | "https://github.com/mirage/alcotest/releases/download/1.6.0/alcotest-1.6.0.tbz" 35 | ] 36 | [ 37 | "astring.0.8.5+dune" 38 | "https://github.com/dune-universe/astring/archive/v0.8.5+dune.tar.gz" 39 | ] 40 | [ 41 | "base-bytes.base+dune" 42 | "https://github.com/kit-ty-kate/bytes/archive/v0.1.0.tar.gz" 43 | ] 44 | [ 45 | "cmdliner.1.1.1+dune" 46 | "https://erratique.ch/software/cmdliner/releases/cmdliner-1.1.1.tbz" 47 | ] 48 | [ 49 | "cppo.1.6.9" 50 | "https://github.com/ocaml-community/cppo/archive/v1.6.9.tar.gz" 51 | ] 52 | [ 53 | "csexp.1.5.1" 54 | "https://github.com/ocaml-dune/csexp/releases/download/1.5.1/csexp-1.5.1.tbz" 55 | ] 56 | [ 57 | "dune-configurator.3.5.0" 58 | "https://github.com/ocaml/dune/releases/download/3.5.0/dune-3.5.0.tbz" 59 | ] 60 | [ 61 | "fmt.0.9.0+dune" 62 | "https://github.com/dune-universe/fmt/releases/download/v0.9.0%2Bdune/fmt-0.9.0.dune.tbz" 63 | ] 64 | [ 65 | "logs.0.7.0+dune2" 66 | "https://github.com/dune-universe/logs/releases/download/v0.7.0%2Bdune2/logs-0.7.0.dune2.tbz" 67 | ] 68 | ["lwt.5.6.1" "https://github.com/ocsigen/lwt/archive/5.6.1.tar.gz"] 69 | [ 70 | "ocaml-syntax-shims.1.0.0" 71 | "https://github.com/ocaml-ppx/ocaml-syntax-shims/releases/download/1.0.0/ocaml-syntax-shims-1.0.0.tbz" 72 | ] 73 | [ 74 | "ocplib-endian.1.2" 75 | "https://github.com/OCamlPro/ocplib-endian/archive/refs/tags/1.2.tar.gz" 76 | ] 77 | [ 78 | "omd.2.0.0~alpha2" 79 | "https://github.com/ocaml/omd/releases/download/2.0.0.alpha2/omd-2.0.0.alpha2.tbz" 80 | ] 81 | [ 82 | "re.1.10.4" 83 | "https://github.com/ocaml/ocaml-re/releases/download/1.10.4/re-1.10.4.tbz" 84 | ] 85 | ["seq.base+dune" "https://github.com/c-cube/seq/archive/0.2.2.tar.gz"] 86 | [ 87 | "stdlib-shims.0.3.0" 88 | "https://github.com/ocaml/stdlib-shims/releases/download/0.3.0/stdlib-shims-0.3.0.tbz" 89 | ] 90 | [ 91 | "uutf.1.0.3+dune" 92 | "https://github.com/dune-universe/uutf/releases/download/v1.0.3%2Bdune/uutf-1.0.3.dune.tbz" 93 | ] 94 | ] 95 | x-opam-monorepo-duniverse-dirs: [ 96 | [ 97 | "https://erratique.ch/software/cmdliner/releases/cmdliner-1.1.1.tbz" 98 | "cmdliner" 99 | [ 100 | "sha512=5478ad833da254b5587b3746e3a8493e66e867a081ac0f653a901cc8a7d944f66e4387592215ce25d939be76f281c4785702f54d4a74b1700bc8838a62255c9e" 101 | ] 102 | ] 103 | [ 104 | "https://github.com/OCamlPro/ocplib-endian/archive/refs/tags/1.2.tar.gz" 105 | "ocplib-endian" 106 | [ 107 | "md5=8d5492eeb7c6815ade72a7415ea30949" 108 | "sha512=2e70be5f3d6e377485c60664a0e235c3b9b24a8d6b6a03895d092c6e40d53810bfe1f292ee69e5181ce6daa8a582bfe3d59f3af889f417134f658812be5b8b85" 109 | ] 110 | ] 111 | [ 112 | "https://github.com/c-cube/seq/archive/0.2.2.tar.gz" 113 | "seq" 114 | [ 115 | "md5=9033e02283aa3bde9f97f24e632902e3" 116 | "sha512=cab0eb4cb6d9788b7cbd7acbefefc15689d706c97ff7f75dd97faf3c21e466af4d0ff110541a24729db587e7172b1a30a3c2967e17ec2e49cbd923360052c07c" 117 | ] 118 | ] 119 | [ 120 | "https://github.com/dune-universe/astring/archive/v0.8.5+dune.tar.gz" 121 | "astring" 122 | [ 123 | "sha256=11327c202fd0115f3a2bf7710c9c603b979a32ba9b16c1a64ba155857233acc8" 124 | ] 125 | ] 126 | [ 127 | "https://github.com/dune-universe/fmt/releases/download/v0.9.0%2Bdune/fmt-0.9.0.dune.tbz" 128 | "fmt" 129 | [ 130 | "sha256=844ce674b3146aaf9c14088a0b817cef10c7152054d3cc984543463da978ff81" 131 | "sha512=27765423f43bdfbbdee50906faad14ecf653aaf2fdfc4db6b94791460aa32f3a3490b9d0c1a04aa3ecb0ac4333ea7ce5054251a67a0d67b64f3eb6d737afbf93" 132 | ] 133 | ] 134 | [ 135 | "https://github.com/dune-universe/logs/releases/download/v0.7.0%2Bdune2/logs-0.7.0.dune2.tbz" 136 | "logs" 137 | [ 138 | "sha256=ae2f76b6bb42122371041d389d0d4348346a79b38ffbb7c20d08e85df2fedf76" 139 | "sha512=4c1fdc23c5f9709d50fa1ee518e2ec4cf1a35fb1cedf466bcc849ae47c113b317db2bf95c788d48faacb67952d942d4b378904e3c37e71ef7babb56e2f11ce8b" 140 | ] 141 | ] 142 | [ 143 | "https://github.com/dune-universe/uutf/releases/download/v1.0.3%2Bdune/uutf-1.0.3.dune.tbz" 144 | "uutf" 145 | [ 146 | "sha256=a207104302c6025b32377e6b4f046a037c56e3de12ce7eacd44c2f31ce71649d" 147 | "sha512=7f8904668a37f39a0a61d63539c0afb55d5127e57e0b4ea7ce944216d8d299e44b0f13972ad55f973c93a659ee0f97cf0f1421a7012a15be4c719ee9f9cd857d" 148 | ] 149 | ] 150 | [ 151 | "https://github.com/kit-ty-kate/bytes/archive/v0.1.0.tar.gz" 152 | "bytes" 153 | [ 154 | "sha256=795b9bf545841714aaf0e517b62834a589937f65ad815ed4589ea56fa614d238" 155 | ] 156 | ] 157 | [ 158 | "https://github.com/mirage/alcotest/releases/download/1.6.0/alcotest-1.6.0.tbz" 159 | "alcotest" 160 | [ 161 | "sha256=fd00f9668395874ff3b1d7ef566d14efc02fa7dd34123eb25d59355be94b2329" 162 | "sha512=69a7ef300ba10a9ccb1e25b1cfdb0a0abf9ca976864a52a22f0e1fae1e5d1cbeb99498c086230b839ee9da4d0fd71e63686e126ca42221537f3fdb6f6c5aae95" 163 | ] 164 | ] 165 | [ 166 | "https://github.com/ocaml-community/cppo/archive/v1.6.9.tar.gz" 167 | "cppo" 168 | [ 169 | "md5=d23ffe85ac7dc8f0afd1ddf622770d09" 170 | "sha512=26ff5a7b7f38c460661974b23ca190f0feae3a99f1974e0fd12ccf08745bd7d91b7bc168c70a5385b837bfff9530e0e4e41cf269f23dd8cf16ca658008244b44" 171 | ] 172 | ] 173 | [ 174 | "https://github.com/ocaml-dune/csexp/releases/download/1.5.1/csexp-1.5.1.tbz" 175 | "csexp" 176 | [ 177 | "sha256=d605e4065fa90a58800440ef2f33a2d931398bf2c22061a8acb7df845c0aac02" 178 | "sha512=d785bbabaff9f6bf601399149ef0a42e5e99647b54e27f97ef1625907793dda22a45bf83e0e8a1eba2c63634c5484b54739ff0904ef556f5fc592efa38af7505" 179 | ] 180 | ] 181 | [ 182 | "https://github.com/ocaml-ppx/ocaml-syntax-shims/releases/download/1.0.0/ocaml-syntax-shims-1.0.0.tbz" 183 | "ocaml-syntax-shims" 184 | [ 185 | "sha256=89b2e193e90a0c168b6ec5ddf6fef09033681bdcb64e11913c97440a2722e8c8" 186 | "sha512=75c4c6b0bfa1267a8a49a82ba494d08cf0823fc8350863d6d3d4971528cb09e5a2a29e2981d04c75e76ad0f49360b05a432c9efeff9a4fbc1ec6b28960399852" 187 | ] 188 | ] 189 | [ 190 | "https://github.com/ocaml/dune/releases/download/3.5.0/dune-3.5.0.tbz" 191 | "dune_" 192 | [ 193 | "sha256=77bd4c6704359fae1969636cfc3cd7a517ba3604819ef89c919c0762b5093610" 194 | "sha512=acaed76ab8618977118579641a1f6734ed4a225ab46494c6c5fd8e1bf9a0889e62db9adc7bd11770da602f4dd4785cef5ece4ad26512d08b64b8f3bd8954c80d" 195 | ] 196 | ] 197 | [ 198 | "https://github.com/ocaml/ocaml-re/releases/download/1.10.4/re-1.10.4.tbz" 199 | "ocaml-re" 200 | [ 201 | "sha256=83eb3e4300aa9b1dc7820749010f4362ea83524742130524d78c20ce99ca747c" 202 | "sha512=92b05cf92c389fa8c753f2acca837b15dd05a4a2e8e2bec7a269d2e14c35b1a786d394258376648f80b4b99250ba1900cfe68230b8385aeac153149d9ce56099" 203 | ] 204 | ] 205 | [ 206 | "https://github.com/ocaml/omd/releases/download/2.0.0.alpha2/omd-2.0.0.alpha2.tbz" 207 | "omd" 208 | [ 209 | "sha256=bee39a6fbb5e32efbbc7eb81574f6472d568f2cb37ba93f2de188d4b68fc7396" 210 | "sha512=82c8716774f756071c8c9dbce838309cfc67d2b607b30d9f5add1307fa8330db8951bfd5406aef1dc3b8902ee67b6d43f76687257927944f118f79be7ea24ff1" 211 | ] 212 | ] 213 | [ 214 | "https://github.com/ocaml/stdlib-shims/releases/download/0.3.0/stdlib-shims-0.3.0.tbz" 215 | "stdlib-shims" 216 | [ 217 | "sha256=babf72d3917b86f707885f0c5528e36c63fccb698f4b46cf2bab5c7ccdd6d84a" 218 | "sha512=1151d7edc8923516e9a36995a3f8938d323aaade759ad349ed15d6d8501db61ffbe63277e97c4d86149cf371306ac23df0f581ec7e02611f58335126e1870980" 219 | ] 220 | ] 221 | [ 222 | "https://github.com/ocsigen/lwt/archive/5.6.1.tar.gz" 223 | "lwt" 224 | [ 225 | "md5=279024789a0ec84a9d97d98bad847f97" 226 | "sha512=698875bd3bfcd5baa47eb48e412f442d289f9972421321541860ebe110b9af1949c3fbc253768495726ec547fe4ba25483cd97ff39bc668496fba95b2ed9edd8" 227 | ] 228 | ] 229 | ] 230 | x-opam-monorepo-root-packages: ["toc"] 231 | x-opam-monorepo-version: "0.3" 232 | --------------------------------------------------------------------------------