Handle Jekyll-format files in OCaml
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

initial import

+668
+17
.gitignore
··· 1 + # OCaml build artifacts 2 + _build/ 3 + *.install 4 + *.merlin 5 + 6 + # Third-party sources (fetch locally with opam source) 7 + third_party/ 8 + 9 + # Editor and OS files 10 + .DS_Store 11 + *.swp 12 + *~ 13 + .vscode/ 14 + .idea/ 15 + 16 + # Opam local switch 17 + _opam/
+1
.ocamlformat
··· 1 + version=0.28.1
+53
.tangled/workflows/build.yml
··· 1 + when: 2 + - event: ["push", "pull_request"] 3 + branch: ["main"] 4 + 5 + engine: nixery 6 + 7 + dependencies: 8 + nixpkgs: 9 + - shell 10 + - stdenv 11 + - findutils 12 + - binutils 13 + - libunwind 14 + - ncurses 15 + - opam 16 + - git 17 + - gawk 18 + - gnupatch 19 + - gnum4 20 + - gnumake 21 + - gnutar 22 + - gnused 23 + - gnugrep 24 + - diffutils 25 + - gzip 26 + - bzip2 27 + - gcc 28 + - ocaml 29 + - pkg-config 30 + 31 + steps: 32 + - name: opam 33 + command: | 34 + opam init --disable-sandboxing -a -y 35 + - name: repo 36 + command: | 37 + opam repo add aoah https://tangled.org/anil.recoil.org/aoah-opam-repo.git 38 + - name: switch 39 + command: | 40 + opam install . --confirm-level=unsafe-yes --deps-only 41 + - name: build 42 + command: | 43 + opam exec -- dune build 44 + - name: switch-test 45 + command: | 46 + opam install . --confirm-level=unsafe-yes --deps-only --with-test 47 + - name: test 48 + command: | 49 + opam exec -- dune runtest --verbose 50 + - name: doc 51 + command: | 52 + opam install -y odoc 53 + opam exec -- dune build @doc
+15
LICENSE.md
··· 1 + ISC License 2 + 3 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org> 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.
+67
README.md
··· 1 + # frontmatter - Parse YAML Frontmatter from Markdown 2 + 3 + An OCaml library for parsing YAML frontmatter (Jekyll-format) from Markdown files. Supports extracting structured metadata and body content from files with YAML headers delimited by `---` markers. 4 + 5 + ## Key Features 6 + 7 + - Parse Jekyll-style YAML frontmatter from Markdown documents 8 + - Type-safe extraction of metadata fields using jsont codecs 9 + - Support for common frontmatter fields (title, date, tags, etc.) 10 + - Eio-based file I/O support via `frontmatter-eio` package 11 + 12 + ## Usage 13 + 14 + ```ocaml 15 + (* Parse frontmatter from a string *) 16 + let content = {|--- 17 + title: My Post 18 + date: 2025-01-15 19 + tags: 20 + - ocaml 21 + - tutorial 22 + --- 23 + 24 + # Hello World 25 + 26 + This is the body content. 27 + |} 28 + 29 + let () = 30 + match Frontmatter.of_string content with 31 + | Ok doc -> 32 + let title = Frontmatter.get_string "title" doc in 33 + let body = Frontmatter.body doc in 34 + Printf.printf "Title: %s\nBody: %s\n" 35 + (Option.value ~default:"untitled" title) 36 + body 37 + | Error e -> 38 + Printf.eprintf "Parse error: %s\n" e 39 + ``` 40 + 41 + With Eio file I/O: 42 + 43 + ```ocaml 44 + let () = 45 + Eio_main.run @@ fun env -> 46 + let doc = Frontmatter_eio.read_file env#fs "posts/my-post.md" in 47 + (* ... process document ... *) 48 + ``` 49 + 50 + ## Installation 51 + 52 + ``` 53 + opam install frontmatter frontmatter-eio 54 + ``` 55 + 56 + ## Documentation 57 + 58 + API documentation is available via: 59 + 60 + ``` 61 + opam install frontmatter 62 + odig doc frontmatter 63 + ``` 64 + 65 + ## License 66 + 67 + ISC
+4
dune
··· 1 + ; Root dune file 2 + 3 + ; Ignore third_party directory (for fetched dependency sources) 4 + (data_only_dirs third_party)
+37
dune-project
··· 1 + (lang dune 3.20) 2 + 3 + (name frontmatter) 4 + 5 + (generate_opam_files true) 6 + 7 + (license ISC) 8 + (authors "Anil Madhavapeddy") 9 + (homepage "https://tangled.org/@anil.recoil.org/ocaml-frontmatter") 10 + (maintainers "Anil Madhavapeddy <anil@recoil.org>") 11 + (bug_reports "https://tangled.org/@anil.recoil.org/ocaml-frontmatter/issues") 12 + (maintenance_intent "(latest)") 13 + 14 + (package 15 + (name frontmatter) 16 + (synopsis "Parse YAML frontmatter from Markdown files") 17 + (description 18 + "A library for parsing YAML frontmatter (Jekyll-format) from Markdown files. 19 + Supports extracting structured metadata and body content from files with 20 + YAML headers delimited by '---' markers.") 21 + (depends 22 + (ocaml (>= 5.2)) 23 + (yamlrw (>= 0.3)) 24 + (jsont (>= 0.1)) 25 + (ptime (>= 1.2)) 26 + (odoc :with-doc))) 27 + 28 + (package 29 + (name frontmatter-eio) 30 + (synopsis "Eio file I/O support for frontmatter") 31 + (description 32 + "Eio-based file operations for reading frontmatter files from disk.") 33 + (depends 34 + (ocaml (>= 5.2)) 35 + (frontmatter (= :version)) 36 + (eio (>= 1.2)) 37 + (odoc :with-doc)))
+32
frontmatter-eio.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "Eio file I/O support for frontmatter" 4 + description: 5 + "Eio-based file operations for reading frontmatter files from disk." 6 + maintainer: ["Anil Madhavapeddy <anil@recoil.org>"] 7 + authors: ["Anil Madhavapeddy"] 8 + license: "ISC" 9 + homepage: "https://tangled.org/@anil.recoil.org/ocaml-frontmatter" 10 + bug-reports: "https://tangled.org/@anil.recoil.org/ocaml-frontmatter/issues" 11 + depends: [ 12 + "dune" {>= "3.20"} 13 + "ocaml" {>= "5.2"} 14 + "frontmatter" {= version} 15 + "eio" {>= "1.2"} 16 + "odoc" {with-doc} 17 + ] 18 + build: [ 19 + ["dune" "subst"] {dev} 20 + [ 21 + "dune" 22 + "build" 23 + "-p" 24 + name 25 + "-j" 26 + jobs 27 + "@install" 28 + "@runtest" {with-test} 29 + "@doc" {with-doc} 30 + ] 31 + ] 32 + x-maintenance-intent: ["(latest)"]
+35
frontmatter.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "Parse YAML frontmatter from Markdown files" 4 + description: """ 5 + A library for parsing YAML frontmatter (Jekyll-format) from Markdown files. 6 + Supports extracting structured metadata and body content from files with 7 + YAML headers delimited by '---' markers.""" 8 + maintainer: ["Anil Madhavapeddy <anil@recoil.org>"] 9 + authors: ["Anil Madhavapeddy"] 10 + license: "ISC" 11 + homepage: "https://tangled.org/@anil.recoil.org/ocaml-frontmatter" 12 + bug-reports: "https://tangled.org/@anil.recoil.org/ocaml-frontmatter/issues" 13 + depends: [ 14 + "dune" {>= "3.20"} 15 + "ocaml" {>= "5.2"} 16 + "yamlrw" {>= "0.3"} 17 + "jsont" {>= "0.1"} 18 + "ptime" {>= "1.2"} 19 + "odoc" {with-doc} 20 + ] 21 + build: [ 22 + ["dune" "subst"] {dev} 23 + [ 24 + "dune" 25 + "build" 26 + "-p" 27 + name 28 + "-j" 29 + jobs 30 + "@install" 31 + "@runtest" {with-test} 32 + "@doc" {with-doc} 33 + ] 34 + ] 35 + x-maintenance-intent: ["(latest)"]
+4
lib/dune
··· 1 + (library 2 + (name frontmatter) 3 + (public_name frontmatter) 4 + (libraries yamlrw yamlt ptime))
+118
lib/frontmatter.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + type yaml = Yamlrw.value 7 + 8 + type t = { 9 + yaml : yaml; 10 + body : string; 11 + fname : string option; 12 + } 13 + 14 + let yaml { yaml; _ } = yaml 15 + let body { body; _ } = body 16 + let fname { fname; _ } = fname 17 + 18 + let error_with_fname fname msg = 19 + let prefix = Option.fold ~none:"" ~some:(fun f -> f ^ ": ") fname in 20 + Error (prefix ^ msg) 21 + 22 + (** Parse Jekyll-style date prefix from filename. 23 + Handles: 2025-01-15-slug.md or just slug.md *) 24 + let parse_date_prefix s = 25 + let len = String.length s in 26 + if len >= 11 then 27 + try 28 + let year = int_of_string (String.sub s 0 4) in 29 + let month = int_of_string (String.sub s 5 2) in 30 + let day = int_of_string (String.sub s 8 2) in 31 + if s.[4] = '-' && s.[7] = '-' && s.[10] = '-' then 32 + match Ptime.of_date (year, month, day) with 33 + | Some date -> Some (date, String.sub s 11 (len - 11)) 34 + | None -> None 35 + else None 36 + with _ -> None 37 + else None 38 + 39 + let slug_of_fname fname = 40 + let basename = Filename.basename fname in 41 + let no_ext = Filename.chop_extension basename in 42 + match parse_date_prefix no_ext with 43 + | Some (date, slug) -> Ok (slug, Some date) 44 + | None -> Ok (no_ext, None) 45 + 46 + (** Parse frontmatter using yamlrw's streaming parser. 47 + Uses multi-document support to find the document boundary, 48 + then extracts the body from the byte position. *) 49 + let of_string ?fname content = 50 + (* Check for opening delimiter *) 51 + let content_trimmed = String.trim content in 52 + if not (String.length content_trimmed >= 3 && String.sub content_trimmed 0 3 = "---") then 53 + error_with_fname fname "Content does not start with '---' frontmatter delimiter" 54 + else 55 + let parser = Yamlrw.Parser.of_string content in 56 + let end_pos = ref 0 in 57 + (* Wrap parser to track Document_end position *) 58 + let next_with_tracking () = 59 + match Yamlrw.Parser.next parser with 60 + | None -> None 61 + | Some ev as result -> 62 + (match ev.event with 63 + | Yamlrw.Event.Document_end _ -> 64 + end_pos := ev.span.stop.Yamlrw.Position.index 65 + | _ -> ()); 66 + result 67 + in 68 + try 69 + let yaml = Yamlrw.Loader.value_of_parser next_with_tracking in 70 + let body_start = !end_pos in 71 + (* Skip leading newline after document end marker *) 72 + let body_start = 73 + if body_start < String.length content && content.[body_start] = '\n' 74 + then body_start + 1 75 + else body_start 76 + in 77 + let body = String.sub content body_start (String.length content - body_start) in 78 + Ok { yaml; body; fname } 79 + with Yamlrw.Yamlrw_error e -> 80 + error_with_fname fname ("YAML parse error: " ^ Yamlrw.Error.to_string e) 81 + 82 + let of_string_exn ?fname content = 83 + match of_string ?fname content with 84 + | Ok t -> t 85 + | Error msg -> failwith msg 86 + 87 + let find key { yaml; _ } = 88 + match yaml with 89 + | `O fields -> List.assoc_opt key fields 90 + | _ -> None 91 + 92 + let find_string key t = 93 + Option.bind (find key t) (function `String s -> Some s | _ -> None) 94 + 95 + let find_strings key t = 96 + find key t 97 + |> Option.map (function 98 + | `A items -> List.filter_map (function `String s -> Some s | _ -> None) items 99 + | _ -> []) 100 + |> Option.value ~default:[] 101 + 102 + let find_bool key t = 103 + Option.bind (find key t) (function `Bool b -> Some b | _ -> None) 104 + 105 + let find_int key t = 106 + Option.bind (find key t) (function 107 + | `Float f when Float.is_integer f -> Some (int_of_float f) 108 + | _ -> None) 109 + 110 + let find_float key t = 111 + Option.bind (find key t) (function `Float f -> Some f | _ -> None) 112 + 113 + let decode jsont { yaml; _ } = Yamlt.decode_value jsont yaml 114 + 115 + let decode_exn jsont t = 116 + match decode jsont t with 117 + | Ok v -> v 118 + | Error msg -> failwith msg
+134
lib/frontmatter.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Parse YAML frontmatter from Markdown files (Jekyll-format). 7 + 8 + This library parses files with YAML frontmatter headers delimited by 9 + '---' markers: 10 + 11 + {v 12 + --- 13 + title: My Post 14 + date: 2025-01-15 15 + tags: 16 + - ocaml 17 + - programming 18 + --- 19 + 20 + The body content starts here. 21 + v} 22 + 23 + {1 Basic Usage} 24 + 25 + {[ 26 + match Frontmatter.of_string content with 27 + | Ok fm -> 28 + let title = Frontmatter.find_string "title" fm in 29 + let body = Frontmatter.body fm in 30 + ... 31 + | Error msg -> Printf.eprintf "Parse error: %s\n" msg 32 + ]} 33 + 34 + {1 Typed Decoding} 35 + 36 + For structured access, use Jsont codecs: 37 + 38 + {[ 39 + type post = { title: string; date: Ptime.t; tags: string list } 40 + 41 + let post_jsont = 42 + Jsont.Object.map ~kind:"post" 43 + (fun title date tags -> { title; date; tags }) 44 + |> Jsont.Object.mem "title" Jsont.string ~enc:(fun p -> p.title) 45 + |> Jsont.Object.mem "date" ptime_jsont ~enc:(fun p -> p.date) 46 + |> Jsont.Object.mem "tags" Jsont.(list string) ~dec_absent:[] 47 + ~enc:(fun p -> p.tags) 48 + |> Jsont.Object.finish 49 + 50 + let post = Frontmatter.decode post_jsont fm 51 + ]} 52 + *) 53 + 54 + (** {1 Types} *) 55 + 56 + type t 57 + (** A parsed frontmatter document. *) 58 + 59 + type yaml = Yamlrw.value 60 + (** YAML value type from yamlrw. *) 61 + 62 + (** {1 Parsing} *) 63 + 64 + val of_string : ?fname:string -> string -> (t, string) result 65 + (** Parse a string containing YAML frontmatter. 66 + 67 + The input should have YAML delimited by '---' markers at the start. 68 + Everything after the closing '---' is the body. 69 + 70 + @param fname Optional filename for error messages. 71 + @return Parsed frontmatter or an error message. *) 72 + 73 + val of_string_exn : ?fname:string -> string -> t 74 + (** Like {!of_string} but raises [Failure] on parse error. *) 75 + 76 + (** {1 Accessors} *) 77 + 78 + val yaml : t -> yaml 79 + (** Get the raw YAML value from the frontmatter. *) 80 + 81 + val body : t -> string 82 + (** Get the body content after the frontmatter. *) 83 + 84 + val fname : t -> string option 85 + (** Get the filename if one was provided during parsing. *) 86 + 87 + (** {1 Field Access} 88 + 89 + Convenience functions for accessing common field types. *) 90 + 91 + val find : string -> t -> yaml option 92 + (** [find key fm] looks up [key] in the frontmatter YAML. *) 93 + 94 + val find_string : string -> t -> string option 95 + (** [find_string key fm] gets a string field from frontmatter. *) 96 + 97 + val find_strings : string -> t -> string list 98 + (** [find_strings key fm] gets a string list field, returning empty list 99 + if not found or not a list. *) 100 + 101 + val find_bool : string -> t -> bool option 102 + (** [find_bool key fm] gets a boolean field. *) 103 + 104 + val find_int : string -> t -> int option 105 + (** [find_int key fm] gets an integer field. *) 106 + 107 + val find_float : string -> t -> float option 108 + (** [find_float key fm] gets a float field. *) 109 + 110 + (** {1 Typed Decoding} 111 + 112 + Decode frontmatter using Jsont codecs for structured access. *) 113 + 114 + val decode : 'a Jsont.t -> t -> ('a, string) result 115 + (** [decode jsont fm] decodes the frontmatter YAML using the given codec. 116 + 117 + Uses {!Yamlt.decode_value} to interpret the YAML value directly through 118 + the Jsont codec. *) 119 + 120 + val decode_exn : 'a Jsont.t -> t -> 'a 121 + (** Like {!decode} but raises [Failure] on decode error. *) 122 + 123 + (** {1 Slug Extraction} 124 + 125 + Jekyll-style filename slug extraction. *) 126 + 127 + val slug_of_fname : string -> (string * Ptime.t option, string) result 128 + (** Extract slug and optional date from a Jekyll-style filename. 129 + 130 + Handles formats like: 131 + - [2025-01-15-my-post.md] -> [("my-post", Some date)] 132 + - [my-post.md] -> [("my-post", None)] 133 + 134 + @return Tuple of (slug, optional date) or error message. *)
+4
lib_eio/dune
··· 1 + (library 2 + (name frontmatter_eio) 3 + (public_name frontmatter-eio) 4 + (libraries frontmatter eio))
+22
lib_eio/frontmatter_eio.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + let read_string fs path = 7 + Eio.Path.(load (fs / path)) 8 + 9 + let of_file fs path = 10 + try 11 + let content = read_string fs path in 12 + Frontmatter.of_string ~fname:path content 13 + with 14 + | Eio.Io (Eio.Fs.E (Eio.Fs.Not_found _), _) -> 15 + Error (Printf.sprintf "File not found: %s" path) 16 + | Eio.Io (_, _) as e -> 17 + Error (Printf.sprintf "Error reading %s: %s" path (Printexc.to_string e)) 18 + 19 + let of_file_exn fs path = 20 + match of_file fs path with 21 + | Ok t -> t 22 + | Error msg -> failwith msg
+34
lib_eio/frontmatter_eio.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Eio file I/O support for frontmatter. 7 + 8 + {1 Reading Files} 9 + 10 + {[ 11 + Eio_main.run @@ fun env -> 12 + let fs = Eio.Stdenv.fs env in 13 + match Frontmatter_eio.of_file fs "posts/2025-01-15-my-post.md" with 14 + | Ok fm -> 15 + let title = Frontmatter.find_string "title" fm in 16 + ... 17 + | Error msg -> Printf.eprintf "Error: %s\n" msg 18 + ]} 19 + *) 20 + 21 + val of_file : _ Eio.Path.t -> string -> (Frontmatter.t, string) result 22 + (** [of_file fs path] reads and parses a frontmatter file. 23 + 24 + @param fs Eio filesystem capability 25 + @param path Path to the file to read 26 + @return Parsed frontmatter or error message *) 27 + 28 + val of_file_exn : _ Eio.Path.t -> string -> Frontmatter.t 29 + (** Like {!of_file} but raises [Failure] on error. *) 30 + 31 + val read_string : _ Eio.Path.t -> string -> string 32 + (** [read_string fs path] reads a file as a string. 33 + 34 + Helper function for reading file contents. *)
+3
test/dune
··· 1 + (test 2 + (name test_frontmatter) 3 + (libraries frontmatter))
+88
test/test_frontmatter.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + let test_basic () = 7 + let content = {|--- 8 + title: Hello World 9 + tags: 10 + - ocaml 11 + - yaml 12 + --- 13 + # Markdown Body 14 + 15 + This is the body content. 16 + |} in 17 + match Frontmatter.of_string content with 18 + | Error e -> 19 + Printf.printf "ERROR: %s\n" e; 20 + false 21 + | Ok t -> 22 + let title = Option.value ~default:"(none)" (Frontmatter.find_string "title" t) in 23 + let tags = String.concat ", " (Frontmatter.find_strings "tags" t) in 24 + let body = Frontmatter.body t in 25 + Printf.printf "Title: %s\n" title; 26 + Printf.printf "Tags: %s\n" tags; 27 + Printf.printf "Body:\n%s\n" body; 28 + title = "Hello World" && 29 + tags = "ocaml, yaml" && 30 + String.length body > 0 31 + 32 + let test_no_frontmatter () = 33 + let content = "No frontmatter here" in 34 + match Frontmatter.of_string content with 35 + | Error _ -> 36 + Printf.printf "Correctly rejected content without frontmatter\n"; 37 + true 38 + | Ok _ -> 39 + Printf.printf "ERROR: Should have rejected content without frontmatter\n"; 40 + false 41 + 42 + let test_with_dash_in_body () = 43 + let content = {|--- 44 + title: Test 45 + --- 46 + Body with --- in it 47 + And more content 48 + |} in 49 + match Frontmatter.of_string content with 50 + | Error e -> 51 + Printf.printf "ERROR: %s\n" e; 52 + false 53 + | Ok t -> 54 + let body = Frontmatter.body t in 55 + Printf.printf "Body with dashes: %s\n" body; 56 + String.sub body 0 4 = "Body" 57 + 58 + let test_explicit_doc_end () = 59 + let content = {|--- 60 + title: With explicit end 61 + ... 62 + Body after explicit document end marker 63 + |} in 64 + match Frontmatter.of_string content with 65 + | Error e -> 66 + Printf.printf "ERROR: %s\n" e; 67 + false 68 + | Ok t -> 69 + let body = Frontmatter.body t in 70 + Printf.printf "Body after ...: %s\n" body; 71 + String.sub body 0 4 = "Body" 72 + 73 + let () = 74 + Printf.printf "=== Testing basic frontmatter ===\n"; 75 + let r1 = test_basic () in 76 + Printf.printf "\n=== Testing no frontmatter ===\n"; 77 + let r2 = test_no_frontmatter () in 78 + Printf.printf "\n=== Testing dash in body ===\n"; 79 + let r3 = test_with_dash_in_body () in 80 + Printf.printf "\n=== Testing explicit doc end ===\n"; 81 + let r4 = test_explicit_doc_end () in 82 + if r1 && r2 && r3 && r4 then ( 83 + Printf.printf "\nAll tests passed!\n"; 84 + exit 0 85 + ) else ( 86 + Printf.printf "\nSome tests failed!\n"; 87 + exit 1 88 + )