package quill
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
Interactive REPL and markdown notebooks
Install
dune-project
Dependency
Authors
Maintainers
Sources
raven-1.0.0.alpha3.tbz
sha256=96d35ce03dfbebd2313657273e24c2e2d20f9e6c7825b8518b69bd1d6ed5870f
sha512=90c5053731d4108f37c19430e45456063e872b04b8a1bbad064c356e1b18e69222de8bfcf4ec14757e71f18164ec6e4630ba770dbcb1291665de5418827d1465
doc/src/quill.project/quill_project.ml.html
Source file quill_project.ml
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(*--------------------------------------------------------------------------- Copyright (c) 2026 The Raven authors. All rights reserved. SPDX-License-Identifier: ISC ---------------------------------------------------------------------------*) type notebook = { title : string; path : string } type toc_item = | Notebook of notebook * toc_item list | Section of string | Separator type config = { title : string option; authors : string list; description : string option; output : string option; edit_url : string option; } type t = { title : string; root : string; toc : toc_item list; config : config } let default_config = { title = None; authors = []; description = None; output = None; edit_url = None; } (* ───── Config parser ───── *) let trim = String.trim let leading_spaces s = let len = String.length s in let rec loop i = if i < len && s.[i] = ' ' then loop (i + 1) else i in loop 0 let is_comment_or_blank s = let s = trim s in String.length s = 0 || s.[0] = '#' let is_separator s = trim s = "---" let is_section s = let s = trim s in let len = String.length s in len >= 2 && s.[0] = '[' && s.[len - 1] = ']' let parse_section s = let s = trim s in String.sub s 1 (String.length s - 2) let parse_kv s = match String.index_opt s '=' with | None -> None | Some i -> let key = trim (String.sub s 0 i) in let value = trim (String.sub s (i + 1) (String.length s - i - 1)) in Some (key, value) let is_toc_entry s = parse_kv s <> None || is_section s || is_separator s let parse_metadata (cfg : config) key value = match key with | "title" -> { cfg with title = Some value } | "authors" -> let = List.map trim (String.split_on_char ',' value) in { cfg with authors } | "description" -> { cfg with description = Some value } | "output" -> { cfg with output = Some value } | "edit-url" -> { cfg with edit_url = Some value } | _ -> cfg (* TOC parser: builds a tree from indented lines. We collect items at each indent level. When indentation increases, new items become children of the previous notebook. When it decreases, we close the current group. *) type toc_entry = | E_notebook of string * string * int (* title, path, indent *) | E_section of string | E_separator let collect_toc_entries lines = let entries = ref [] in List.iter (fun line -> if is_comment_or_blank line then () else if is_separator line then entries := E_separator :: !entries else if is_section line then entries := E_section (parse_section line) :: !entries else let indent = leading_spaces line in match parse_kv line with | Some (title, path) -> entries := E_notebook (title, path, indent) :: !entries | None -> ()) lines; List.rev !entries let rec build_toc entries = match entries with | [] -> ([], []) | entry :: rest -> ( match entry with | E_separator -> let siblings, remaining = build_toc rest in (Separator :: siblings, remaining) | E_section title -> let siblings, remaining = build_toc rest in (Section title :: siblings, remaining) | E_notebook (title, path, indent) -> let children, after_children = collect_children (indent + 1) rest in let nb = { title; path } in let siblings, remaining = build_toc after_children in (Notebook (nb, children) :: siblings, remaining)) and collect_children min_indent entries = match entries with | E_notebook (_, _, indent) :: _ when indent >= min_indent -> let item, rest = take_one_child min_indent entries in let more_children, remaining = collect_children min_indent rest in (item :: more_children, remaining) | _ -> ([], entries) and take_one_child min_indent entries = match entries with | E_notebook (title, path, indent) :: rest when indent >= min_indent -> let children, remaining = collect_children (indent + 1) rest in let nb = { title; path } in (Notebook (nb, children), remaining) | _ -> failwith "take_one_child: expected notebook entry" let parse_config source = let lines = String.split_on_char '\n' source in (* Split into metadata lines and TOC lines *) let in_metadata = ref true in let meta_lines = ref [] in let toc_lines = ref [] in List.iter (fun line -> if !in_metadata then if is_comment_or_blank line then () else if is_toc_entry (String.trim line) && not (is_comment_or_blank line) then ( (* Check if this is a key=value that looks like metadata or TOC *) match parse_kv line with | Some (_, _) when (not (is_section line)) && leading_spaces line = 0 -> (* Could be metadata or a TOC entry. Heuristic: if the value looks like a file path (contains . or /), it's TOC *) let trimmed = trim line in let value = match String.index_opt trimmed '=' with | Some i -> trim (String.sub trimmed (i + 1) (String.length trimmed - i - 1)) | None -> "" in if String.contains value '/' || String.contains value '.' && String.length value > 0 && value <> "" then ( in_metadata := false; toc_lines := line :: !toc_lines) else if value = "" then ( (* Empty value at indent 0: could be a placeholder TOC entry or a metadata key with no value. If we haven't seen any TOC entries yet, check if the key is a known metadata key *) let key = match String.index_opt trimmed '=' with | Some i -> trim (String.sub trimmed 0 i) | None -> trimmed in match key with | "title" | "authors" | "description" | "output" | "edit-url" -> meta_lines := line :: !meta_lines | _ -> in_metadata := false; toc_lines := line :: !toc_lines) else meta_lines := line :: !meta_lines | _ -> in_metadata := false; toc_lines := line :: !toc_lines) else meta_lines := line :: !meta_lines else toc_lines := line :: !toc_lines) lines; let config = List.fold_left (fun cfg line -> match parse_kv line with | Some (key, value) -> parse_metadata cfg key value | None -> cfg) default_config (List.rev !meta_lines) in let toc_entries = collect_toc_entries (List.rev !toc_lines) in let toc, _ = build_toc toc_entries in Ok (config, toc) (* ───── Title from filename ───── *) let title_of_filename path = let base = Filename.basename path in let name = Filename.remove_extension base in (* Strip leading digits and separators *) let len = String.length name in let start = ref 0 in while !start < len && let c = name.[!start] in (c >= '0' && c <= '9') || c = '-' || c = '_' do incr start done; let name = if !start >= len then name else String.sub name !start (len - !start) in (* Replace dashes and underscores with spaces *) let buf = Buffer.create (String.length name) in String.iter (fun c -> match c with | '-' | '_' -> Buffer.add_char buf ' ' | c -> Buffer.add_char buf c) name; let result = Buffer.contents buf in (* Capitalize first letter *) if String.length result > 0 then let first = Char.uppercase_ascii result.[0] in let rest = String.sub result 1 (String.length result - 1) in String.make 1 first ^ rest else result (* ───── Queries ───── *) let rec all_notebooks toc = List.concat_map (fun item -> match item with | Notebook (nb, children) -> nb :: all_notebooks children | Section _ | Separator -> []) toc let is_placeholder nb = nb.path = "" let notebooks project = List.filter (fun nb -> not (is_placeholder nb)) (all_notebooks project.toc) let notebooks_array project = Array.of_list (notebooks project) let find_notebook_index project nb = let nbs = notebooks_array project in let rec loop i = if i >= Array.length nbs then None else if nbs.(i).path = nb.path then Some i else loop (i + 1) in loop 0 let prev_notebook project nb = match find_notebook_index project nb with | Some i when i > 0 -> Some (notebooks_array project).(i - 1) | _ -> None let next_notebook project nb = let nbs = notebooks_array project in match find_notebook_index project nb with | Some i when i < Array.length nbs - 1 -> Some nbs.(i + 1) | _ -> None let number toc nb = let rec search counter = function | [] -> None | Notebook (n, children) :: rest -> incr counter; if n.path = nb.path then Some [ !counter ] else begin match search (ref 0) children with | Some sub -> Some (!counter :: sub) | None -> search counter rest end | Section _ :: rest -> counter := 0; search counter rest | Separator :: rest -> search counter rest in match search (ref 0) toc with Some ns -> ns | None -> [] let number_string = function | [] -> "" | ns -> String.concat "." (List.map string_of_int ns)
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>