this repo has no description
at main 2785 lines 150 kB view raw
1// Tutorial test definitions for OCaml libraries 2// Each entry is a self-contained interactive tutorial 3 4const U = { 5 // ── Bunzli libraries ── 6 'fmt.0.9.0': '9901393f978b0a6627c5eab595111f50', 7 'fmt.0.10.0': 'd8140118651d08430f933d410a909e3b', 8 'fmt.0.11.0': '7663cce356513833b908ae5e4f521106', 9 'cmdliner.1.0.4': '0dd34259dc0892e543b03b3afb0a77fa', 10 'cmdliner.1.3.0': '258e7979b874502ea546e90a0742184a', 11 'cmdliner.2.0.0': '91c3d96cea9b89ddd24cf7b78786a5ca', 12 'cmdliner.2.1.0': 'f3e665d5388ac380a70c5ed67f465bbb', 13 'mtime.1.3.0': 'b6735658fd307bba23a7c5f21519b910', 14 'mtime.1.4.0': 'ebccfc43716c6da0ca4a065e60d0f875', 15 'mtime.2.1.0': '7db699c334606d6f66e65c8b515d298d', 16 'logs.0.7.0': '2c014cfbbee1d278b162002eae03eaa8', 17 'logs.0.10.0': '07a565e7588ce100ffd7c8eb8b52df07', 18 'uucp.14.0.0': '60e1409eb30c0650c4d4cbcf3c453e65', 19 'uucp.15.0.0': '6a96a3f145249f110bf14739c78e758c', 20 'uucp.16.0.0': '2bf0fbf12aa05c8f99989a759d2dc8cf', 21 'uucp.17.0.0': '58b9c48e9528ce99586b138d8f4778c2', 22 'uunf.14.0.0': 'cac36534f1bf353fd2192efd015dd0e6', 23 'uunf.17.0.0': '96704cd9810ea1ed504e4ed71cde82b0', 24 'astring.0.8.5': '1cdbe76f0ec91a6eb12bd0279a394492', 25 'jsonm.1.0.2': 'ac28e00ecd46c9464f5575c461b5d48f', 26 'xmlm.1.4.0': 'c4c22d0db3ea01343c1a868bab35e1b4', 27 'ptime.1.2.0': 'd57c69f3dd88b91454622c1841971354', 28 'react.1.2.2': 'f438ba61693a5448718c73116b228f3c', 29 'hmap.0.8.1': '753d7c421afb866e7ffe07ddea3b8349', 30 'gg.1.0.0': '02a9bababc92d6639cdbaf20233597ba', 31 'note.0.0.3': '2545f914c274aa806d29749eb96836fa', 32 'otfm.0.4.0': '4f870a70ee71e41dff878af7123b2cd6', 33 'vg.0.9.5': '0e2e71cfd8fe2e81bff124849421f662', 34 'bos.0.2.1': '0e04faa6cc5527bc124d8625bded34fc', 35 'fpath.0.7.3': '6c4fe09a631d871865fd38aa15cd61d4', 36 'uutf.1.0.4': 'ac04fa0671533316f94dacbd14ffe0bf', 37 'uuseg.14.0.0': '406ca4903030ee122ff6c61b61446ddc', 38 'uuseg.15.0.0': '62ea8502ec4e6c386a070cc75ec8377a', 39 'uuseg.16.0.0': '3a191102f91addba06efdd712ba037b2', 40 'uuseg.17.0.0': '7d9b8800252a9bec2a9be496e02eb9da', 41 'b0.0.0.6': 'bfc34a228f53ac5ced707eed285a6e5c', 42 43 // ── Serialization ── 44 'yojson.1.7.0': '0273d3484c1256a463fc6b5d822ba4ae', 45 'yojson.2.0.2': 'b02baa519ba5bedf95d1b42b5e66381a', 46 'yojson.2.1.2': '5efcef16114ee98834c3f4cf9a7f45b4', 47 'yojson.2.2.2': '739ca5bed6c1201d906f0f3132274687', 48 'yojson.3.0.0': 'e52f084da1b654e881d2dba81775b440', 49 'ezjsonm.1.1.0': '899976ac0dc15192e669f652bf29f29e', 50 'ezjsonm.1.2.0': 'b93294aee1f9361bfe1916f4127fa56c', 51 'ezjsonm.1.3.0': '98ee39eafcb78d7c102a291c7faa302e', 52 'sexplib0.v0.15.1':'f6fb7feeb446b4a67adb486a2392bf3e', 53 'sexplib0.v0.16.0':'8ec78baf83bdc6a0a181b58efb909869', 54 'sexplib0.v0.17.0':'08fe6d134ac413075564220297b2554f', 55 'csexp.1.5.2': '8443eb56f5227050537a4eb47b26fd10', 56 'base64.3.4.0': '9befb8850a0bcfb0556f8a7d2de8d3bd', 57 'base64.3.5.2': 'dee9a00f3ec355e7dab15121d7cb5a3c', 58 59 // ── Text / Parsing ── 60 're.1.10.4': '4697515ef0ed56df99029cfa8b6a4c1a', 61 're.1.11.0': '87fd99e341a1468e36de4973044ba1cb', 62 're.1.12.0': '3bc6cdc9f1fd39cd5ee61b89f423f51a', 63 're.1.13.2': 'e080307b8290a25f41d4ad87427c3cc0', 64 're.1.14.0': '7f3c1f0452e7156dea56c1a52e2096a4', 65 'angstrom.0.15.0':'12fe7a4d575b34f30551cf6eaaed4a0b', 66 'angstrom.0.16.1':'f46aa50b81b7e6a0dd7ee69d247920c0', 67 'tyre.0.5': 'ffdb349acdd211cf2699a689ed1491d3', 68 'tyre.1.0': 'e413ed92802108a275144c27e0f9efa8', 69 70 // ── Data structures ── 71 'containers.3.17': '62a1dfab4e79dda21e6775fc35bac90b', 72 'iter.1.7': '4aca16dd3c74db420f49a18cc54fd66f', 73 'iter.1.8': '7724b461d742c869a4abfaa870879763', 74 'iter.1.9': '73e7b9c9b638abf269affd1967509ce6', 75 'ocamlgraph.2.0.0':'bcfe5c830a54c4fc55121d6bc69d52d4', 76 'ocamlgraph.2.1.0':'e867dbcc2de571de4cb84d9a45e554bd', 77 'ocamlgraph.2.2.0':'ab9aa04f9746bf7c5b275cfddfc9dc20', 78 79 // ── Crypto / Encoding ── 80 'digestif.1.1.2': '33d25472185fc31bd41d277d488478f2', 81 'digestif.1.3.0': 'c3664212cf01a38aa9af7c54123056cf', 82 'hex.1.4.0': '0bff54cafa851851e4ddb617126d4ce6', 83 'hex.1.5.0': 'a46b45c6915570ff2966d96d9101258c', 84 'eqaf.0.9': '39499417427d1d35a028fc9101ecbfb2', 85 'eqaf.0.10': 'eed017d4f8c09e4fcabf2f9320361e64', 86 87 // ── Networking types ── 88 'uri.4.2.0': '473a4aaa6884b7d04af481a4bcf573e6', 89 'uri.4.4.0': '3f9567317844352b63256b5d7075e595', 90 'ipaddr.5.6.0': 'ca33bd0287b9b4cd9f67a4c6464b0bd9', 91 'ipaddr.5.6.1': '516728912d49b2b8b007b762f0cd985f', 92 'domain-name.0.4.1':'55bf622c2e1dacb9e5c7da2cf9195e95', 93 'domain-name.0.5.0':'e069e9e37be7c2d8264f41a661136c60', 94 95 // ── Math ── 96 'zarith.1.13': '5b98616ce2f37ecfbefd3d8c7c1f45a9', 97 'zarith.1.14': '3abb9b1ae0690526d21d9630f3f27153', 98 99 // ── Testing ── 100 'qcheck-core.0.25':'c338cf74d7ad14da542181619f55fbda', 101 'qcheck-core.0.27':'eb7a98de039353471656e141c6107fc3', 102 'qcheck-core.0.91':'c1307fa49614dc884aa0fec68b55c832', 103}; 104 105// ── Factory: Fmt (same API across 0.9–0.11) ──────────────────────────── 106function fmtTutorial(version, universe) { 107 return { 108 name: 'Fmt', version, opam: 'fmt', 109 description: 'OCaml Format pretty-printer combinators', 110 universe, require: ['fmt'], 111 sections: [ 112 { title: 'String Formatting', 113 description: 'Fmt.str works like Printf.sprintf, building a string from a format string.', 114 steps: [ 115 { code: 'Fmt.str "%d" 42;;', expect: '"42"', 116 description: 'Format an integer into a string' }, 117 { code: 'Fmt.str "Hello, %s!" "world";;', expect: '"Hello, world!"', 118 description: 'Interpolate a string value' }, 119 { code: 'Fmt.str "%d + %d = %d" 1 2 3;;', expect: '"1 + 2 = 3"', 120 description: 'Multiple format arguments' }, 121 { code: 'Fmt.str "%a" Fmt.int 42;;', expect: '"42"', 122 description: 'Use a typed formatter with %a' }, 123 ] }, 124 { title: 'Typed Formatters', 125 description: 'Fmt provides typed formatter values (type \'a Fmt.t = Format.formatter -> \'a -> unit) for common types.', 126 steps: [ 127 { code: 'Fmt.str "%a" Fmt.bool true;;', expect: '"true"', 128 description: 'Format a boolean' }, 129 { code: 'Fmt.str "%a" Fmt.float 3.14;;', expect: '3.14', 130 description: 'Format a float' }, 131 { code: 'Fmt.str "%a" Fmt.string "hi";;', expect: '"hi"', 132 description: 'Format a string with the string formatter' }, 133 ] }, 134 { title: 'Collection Formatters', 135 description: 'Fmt can format lists, options, pairs, and results with configurable separators.', 136 steps: [ 137 { code: 'Fmt.str "%a" Fmt.(list int) [1; 2; 3];;', expect: '1', 138 description: 'Format a list of ints (default separator)' }, 139 { code: 'Fmt.str "%a" Fmt.(list ~sep:comma int) [1; 2; 3];;', expect: '1, 2', 140 description: 'Format a list with comma separators' }, 141 { code: 'Fmt.str "%a" Fmt.(list ~sep:(any " ") int) [10; 20];;', expect: '"10 20"', 142 description: 'Format with space separators using Fmt.any' }, 143 { code: 'Fmt.str "%a" Fmt.(option int) (Some 5);;', expect: '5', 144 description: 'Format an option value' }, 145 { code: 'Fmt.str "%a" Fmt.(option int) None;;', expect: '', 146 description: 'Format None (empty output by default)' }, 147 { code: 'Fmt.str "%a" Fmt.(pair ~sep:comma int string) (42, "hi");;', expect: '42', 148 description: 'Format a pair' }, 149 ] }, 150 { title: 'Output to stdout', 151 description: 'Fmt.pr prints directly to stdout. Use @. for a newline flush.', 152 steps: [ 153 { code: 'Fmt.pr "value: %d@." 42;;', expectStdout: 'value: 42', 154 description: 'Print formatted output to stdout' }, 155 { code: 'Fmt.pr "%a@." Fmt.(list ~sep:sp int) [1; 2; 3];;', expectStdout: '1', 156 description: 'Print a list to stdout' }, 157 ] }, 158 { title: 'Combinators', 159 description: 'Higher-order combinators transform formatters.', 160 steps: [ 161 { code: 'let pp_len = Fmt.using String.length Fmt.int;;', expect: 'Fmt.t', 162 description: 'Fmt.using transforms input before formatting' }, 163 { code: 'Fmt.str "%a" pp_len "hello";;', expect: '"5"', 164 description: 'pp_len formats the length of a string' }, 165 { code: 'Fmt.str "%a" (Fmt.Dump.list Fmt.int) [1; 2; 3];;', expect: '[1; 2; 3]', 166 description: 'Fmt.Dump formats with OCaml syntax (brackets)' }, 167 ] }, 168 ], 169 }; 170} 171 172// ── Factory: Uucp (same API across 14–17, different Unicode version) ─── 173function uucpTutorial(version, universe, unicodeVer) { 174 return { 175 name: 'Uucp', version, opam: 'uucp', 176 description: `Unicode character properties (Unicode ${unicodeVer})`, 177 universe, require: ['uucp'], 178 sections: [ 179 { title: 'Unicode Version', 180 description: 'Each Uucp release tracks a specific Unicode standard version.', 181 steps: [ 182 { code: 'Uucp.unicode_version;;', expect: `"${unicodeVer}"`, 183 description: 'Check which Unicode version this release implements' }, 184 ] }, 185 { title: 'General Category', 186 description: 'Uucp.Gc.general_category returns the Unicode General Category of a character as a polymorphic variant.', 187 steps: [ 188 { code: 'Uucp.Gc.general_category (Uchar.of_int 0x0041);;', expect: '`Lu', 189 description: "'A' (U+0041) is an uppercase letter (Lu)" }, 190 { code: 'Uucp.Gc.general_category (Uchar.of_int 0x0061);;', expect: '`Ll', 191 description: "'a' (U+0061) is a lowercase letter (Ll)" }, 192 { code: 'Uucp.Gc.general_category (Uchar.of_int 0x0030);;', expect: '`Nd', 193 description: "'0' (U+0030) is a decimal digit (Nd)" }, 194 { code: 'Uucp.Gc.general_category (Uchar.of_int 0x0020);;', expect: '`Zs', 195 description: "Space (U+0020) is a space separator (Zs)" }, 196 ] }, 197 { title: 'Script Detection', 198 description: 'Uucp.Script.script identifies which writing system a character belongs to.', 199 steps: [ 200 { code: 'Uucp.Script.script (Uchar.of_int 0x03B1);;', expect: '`Grek', 201 description: "Greek alpha (U+03B1) is in the Greek script" }, 202 { code: 'Uucp.Script.script (Uchar.of_int 0x4E16);;', expect: '`Hani', 203 description: "CJK character (U+4E16) is in the Han script" }, 204 { code: 'Uucp.Script.script (Uchar.of_int 0x0041);;', expect: '`Latn', 205 description: "'A' is in the Latin script" }, 206 ] }, 207 { title: 'Character Properties', 208 description: 'Uucp provides boolean property lookups for whitespace, alphabetic characters, and more.', 209 steps: [ 210 { code: 'Uucp.White.is_white_space (Uchar.of_int 0x0020);;', expect: 'true', 211 description: 'Space is whitespace' }, 212 { code: 'Uucp.White.is_white_space (Uchar.of_int 0x0041);;', expect: 'false', 213 description: "'A' is not whitespace" }, 214 { code: 'Uucp.White.is_white_space (Uchar.of_int 0x00A0);;', expect: 'true', 215 description: 'Non-breaking space (U+00A0) is whitespace' }, 216 ] }, 217 ], 218 }; 219} 220 221// ── Factory: Uunf (same API, different Unicode version) ──────────────── 222function uunfTutorial(version, universe, unicodeVer) { 223 return { 224 name: 'Uunf', version, opam: 'uunf', 225 description: `Unicode text normalization (Unicode ${unicodeVer})`, 226 universe, require: ['uunf'], 227 sections: [ 228 { title: 'Unicode Version', 229 description: 'Each Uunf release implements normalization according to a specific Unicode version.', 230 steps: [ 231 { code: 'Uunf.unicode_version;;', expect: `"${unicodeVer}"`, 232 description: 'Check the Unicode version' }, 233 ] }, 234 { title: 'Normalization Forms', 235 description: 'Unicode defines four normalization forms: NFC, NFD, NFKC, and NFKD. Uunf.create selects which form to use.', 236 steps: [ 237 { code: 'let nfc = Uunf.create `NFC;;', expect: 'Uunf.t', 238 description: 'Create an NFC normalizer' }, 239 { code: 'let nfd = Uunf.create `NFD;;', expect: 'Uunf.t', 240 description: 'Create an NFD normalizer (canonical decomposition)' }, 241 { code: 'let nfkc = Uunf.create `NFKC;;', expect: 'Uunf.t', 242 description: 'Create an NFKC normalizer (compatibility composition)' }, 243 { code: 'let nfkd = Uunf.create `NFKD;;', expect: 'Uunf.t', 244 description: 'Create an NFKD normalizer (compatibility decomposition)' }, 245 ] }, 246 { title: 'Adding Characters', 247 description: 'Feed characters to the normalizer with Uunf.add. It returns `Uchar for output characters and `Await when ready for more input.', 248 steps: [ 249 { code: 'let n = Uunf.create `NFC;;', expect: 'Uunf.t', 250 description: 'Create a fresh NFC normalizer' }, 251 { code: 'Uunf.add n (`Uchar (Uchar.of_int 0x0041));;', expect: '', 252 description: "Add 'A' to the normalizer" }, 253 { code: 'Uunf.add n `End;;', expect: '', 254 description: 'Signal end of input' }, 255 ] }, 256 ], 257 }; 258} 259 260// ── Factory: Mtime 1.x (1.3 and 1.4 share the same API) ─────────────── 261function mtime1_3Tutorial(version, universe) { 262 // Mtime 1.3.0: no named constants (ns/ms/s), uses float conversion functions 263 return { 264 name: 'Mtime', version, opam: 'mtime', 265 description: 'Monotonic wall-clock time for OCaml', 266 universe, require: ['mtime'], 267 sections: [ 268 { title: 'Time Span Basics', 269 description: 'Mtime.Span represents monotonic time durations in nanoseconds.', 270 steps: [ 271 { code: 'Mtime.Span.zero;;', expect: 'Mtime.span', 272 description: 'The zero-length span' }, 273 { code: 'Mtime.Span.one;;', expect: 'Mtime.span', 274 description: 'One nanosecond' }, 275 { code: 'Mtime.Span.max_span;;', expect: 'Mtime.span', 276 description: 'The maximum representable span' }, 277 { code: 'Mtime.Span.of_uint64_ns 1_000_000_000L;;', expect: 'Mtime.span', 278 description: 'Create a 1-second span from nanoseconds' }, 279 ] }, 280 { title: 'Span Arithmetic', 281 description: 'Spans support addition, comparison, and absolute difference.', 282 steps: [ 283 { code: 'let one_sec = Mtime.Span.of_uint64_ns 1_000_000_000L;;', expect: 'Mtime.span', 284 description: 'One second' }, 285 { code: 'let two_sec = Mtime.Span.add one_sec one_sec;;', expect: 'Mtime.span', 286 description: 'Add two spans: 1s + 1s = 2s' }, 287 { code: 'Mtime.Span.to_uint64_ns two_sec;;', expect: '2000000000L', 288 description: '2 seconds in nanoseconds' }, 289 { code: 'Mtime.Span.equal Mtime.Span.zero Mtime.Span.zero;;', expect: 'true', 290 description: 'Zero equals zero' }, 291 { code: 'Mtime.Span.compare one_sec Mtime.Span.zero;;', expect: '1', 292 description: '1 second is greater than zero' }, 293 ] }, 294 { title: 'Float Conversions', 295 description: 'Convert spans to floating-point representations in various units.', 296 steps: [ 297 { code: 'Mtime.Span.to_ns one_sec;;', expect: '1000000000.', 298 description: '1 second = 1e9 nanoseconds' }, 299 { code: 'Mtime.Span.to_ms one_sec;;', expect: '1000.', 300 description: '1 second = 1000 milliseconds' }, 301 { code: 'Mtime.Span.to_s one_sec;;', expect: '1.', 302 description: '1 second as a float' }, 303 { code: 'Mtime.Span.to_us one_sec;;', expect: '1000000.', 304 description: '1 second = 1e6 microseconds' }, 305 ] }, 306 ], 307 }; 308} 309 310function mtime1_4Tutorial(version, universe) { 311 // Mtime 1.4.0: has named constants (ns/ms/s) and is_shorter/is_longer 312 return { 313 name: 'Mtime', version, opam: 'mtime', 314 description: 'Monotonic wall-clock time for OCaml', 315 universe, require: ['mtime'], 316 sections: [ 317 { title: 'Time Span Constants', 318 description: 'Mtime 1.4 added named constants for common time durations.', 319 steps: [ 320 { code: 'Mtime.Span.zero;;', expect: 'Mtime.span', 321 description: 'The zero-length span' }, 322 { code: 'Mtime.Span.ns;;', expect: 'Mtime.span', 323 description: '1 nanosecond' }, 324 { code: 'Mtime.Span.ms;;', expect: 'Mtime.span', 325 description: '1 millisecond' }, 326 { code: 'Mtime.Span.s;;', expect: 'Mtime.span', 327 description: '1 second' }, 328 { code: 'Mtime.Span.min;;', expect: 'Mtime.span', 329 description: '1 minute' }, 330 ] }, 331 { title: 'Span Arithmetic', 332 description: 'Spans support addition, scaling, and comparison.', 333 steps: [ 334 { code: 'let two_sec = Mtime.Span.add Mtime.Span.s Mtime.Span.s;;', expect: 'Mtime.span', 335 description: '1s + 1s = 2s' }, 336 { code: 'Mtime.Span.to_uint64_ns two_sec;;', expect: '2000000000L', 337 description: '2 seconds in nanoseconds' }, 338 { code: 'Mtime.Span.compare Mtime.Span.ms Mtime.Span.s;;', expect: '-1', 339 description: '1ms is less than 1s' }, 340 { code: 'Mtime.Span.equal Mtime.Span.zero Mtime.Span.zero;;', expect: 'true', 341 description: 'Zero equals zero' }, 342 ] }, 343 { title: 'Conversions', 344 description: 'Convert spans to floating-point representations in various units.', 345 steps: [ 346 { code: 'Mtime.Span.to_ms Mtime.Span.s;;', expect: '1000.', 347 description: '1 second = 1000 milliseconds' }, 348 { code: 'Mtime.Span.to_s Mtime.Span.s;;', expect: '1.', 349 description: '1 second as a float' }, 350 { code: 'Mtime.Span.of_uint64_ns 500_000_000L |> Mtime.Span.to_ms;;', expect: '500.', 351 description: '500ms round-trip through nanoseconds' }, 352 ] }, 353 ], 354 }; 355} 356 357export const TUTORIALS = { 358 // ═══════════════════════════════════════════════════════════════════════ 359 // Fmt 360 // ═══════════════════════════════════════════════════════════════════════ 361 'fmt.0.9.0': fmtTutorial('0.9.0', U['fmt.0.9.0']), 362 'fmt.0.10.0': fmtTutorial('0.10.0', U['fmt.0.10.0']), 363 'fmt.0.11.0': fmtTutorial('0.11.0', U['fmt.0.11.0']), 364 365 // ═══════════════════════════════════════════════════════════════════════ 366 // Cmdliner 367 // ═══════════════════════════════════════════════════════════════════════ 368 'cmdliner.1.0.4': { 369 name: 'Cmdliner', version: '1.0.4', opam: 'cmdliner', 370 description: 'Declarative definition of command line interfaces (v1 API)', 371 universe: U['cmdliner.1.0.4'], require: ['cmdliner'], 372 sections: [ 373 { title: 'Argument Info', 374 description: 'Cmdliner.Arg.info describes command-line arguments with names, docs, and metadata.', 375 steps: [ 376 { code: 'let verbose_info = Cmdliner.Arg.info ["v"; "verbose"] ~doc:"Be verbose";;', 377 expect: 'Cmdliner.Arg.info', description: 'Create info for a --verbose/-v flag' }, 378 { code: 'let name_info = Cmdliner.Arg.info [] ~docv:"NAME" ~doc:"The name";;', 379 expect: 'Cmdliner.Arg.info', description: 'Create info for a positional argument' }, 380 ] }, 381 { title: 'Argument Definitions', 382 description: 'Arguments are built from converters + info, then lifted into terms with Arg.value.', 383 steps: [ 384 { code: 'Cmdliner.Arg.string;;', expect: 'string Cmdliner.Arg.conv', 385 description: 'Built-in string converter' }, 386 { code: 'Cmdliner.Arg.int;;', expect: 'int Cmdliner.Arg.conv', 387 description: 'Built-in int converter' }, 388 { code: 'let verbose = Cmdliner.Arg.(value (flag (info ["v";"verbose"])));;', 389 expect: 'bool Cmdliner.Term.t', description: 'Define a boolean flag term' }, 390 { code: 'let count = Cmdliner.Arg.(value (opt int 0 (info ["c";"count"])));;', 391 expect: 'int Cmdliner.Term.t', description: 'Define an optional int argument with default 0' }, 392 ] }, 393 { title: 'Terms (v1 API)', 394 description: 'In Cmdliner 1.0.x, Term.const and Term.($) combine argument terms into a program term.', 395 steps: [ 396 { code: 'let greet = Cmdliner.Term.const (fun v n -> Printf.sprintf "%s%s" (if v then "HI " else "hi ") n);;', 397 expect: 'Cmdliner.Term.t', description: 'A constant function lifted into a term' }, 398 { code: 'Cmdliner.Term.info "greet" ~doc:"A greeting program";;', 399 expect: 'Cmdliner.Term.info', description: 'Term.info describes the command (v1 API)' }, 400 { code: 'Cmdliner.Term.eval;;', expect: 'Term.result', 401 description: 'Term.eval runs a term — available in 1.0.x' }, 402 ] }, 403 ], 404 }, 405 406 'cmdliner.1.3.0': { 407 name: 'Cmdliner', version: '1.3.0', opam: 'cmdliner', 408 description: 'Declarative definition of command line interfaces (transitional)', 409 universe: U['cmdliner.1.3.0'], require: ['cmdliner'], 410 sections: [ 411 { title: 'Argument Building', 412 description: 'Cmdliner 1.3 is a transitional release supporting both the old Term API and the new Cmd API.', 413 steps: [ 414 { code: 'let verbose = Cmdliner.Arg.(value (flag (info ["v";"verbose"] ~doc:"Be verbose")));;', 415 expect: 'bool Cmdliner.Term.t', description: 'Define a verbose flag' }, 416 { code: 'let greeting = Cmdliner.Arg.(value (pos 0 string "world" (info [] ~docv:"NAME")));;', 417 expect: 'string Cmdliner.Term.t', description: 'Define a positional name argument' }, 418 ] }, 419 { title: 'New Cmd API (introduced in 1.1+)', 420 description: 'The Cmd module provides a structured way to define commands, replacing Term.info + Term.eval.', 421 steps: [ 422 { code: 'Cmdliner.Cmd.info "hello" ~doc:"Say hello";;', 423 expect: 'Cmdliner.Cmd.info', description: 'Cmd.info creates command metadata' }, 424 { code: 'let hello_t = Cmdliner.Term.(const (fun v n -> ()) $ verbose $ greeting);;', 425 expect: 'Cmdliner.Term.t', description: 'Combine arguments with Term.const and ($)' }, 426 { code: 'Cmdliner.Cmd.v (Cmdliner.Cmd.info "hello") hello_t;;', 427 expect: 'Cmdliner.Cmd.t', description: 'Create a command from info + term' }, 428 ] }, 429 { title: 'Backward Compatibility', 430 description: 'The old Term.eval API still works in 1.3 for migration.', 431 steps: [ 432 { code: 'Cmdliner.Term.eval;;', expect: 'Term.result', 433 description: 'Term.eval is still available (deprecated but functional)' }, 434 { code: 'Cmdliner.Term.info "test";;', expect: 'Cmdliner.Term.info', 435 description: 'Term.info still works for backward compat' }, 436 ] }, 437 ], 438 }, 439 440 'cmdliner.2.0.0': { 441 name: 'Cmdliner', version: '2.0.0', opam: 'cmdliner', 442 description: 'Declarative definition of command line interfaces (v2 API)', 443 universe: U['cmdliner.2.0.0'], require: ['cmdliner'], 444 sections: [ 445 { title: 'Arguments', 446 description: 'Arguments are defined the same way as in earlier versions.', 447 steps: [ 448 { code: 'let verbose = Cmdliner.Arg.(value (flag (info ["v";"verbose"])));;', 449 expect: 'bool Cmdliner.Term.t', description: 'A boolean flag term' }, 450 { code: 'let name = Cmdliner.Arg.(value (pos 0 string "world" (info [])));;', 451 expect: 'string Cmdliner.Term.t', description: 'A positional string argument' }, 452 ] }, 453 { title: 'Cmd Module (v2 API)', 454 description: 'In Cmdliner 2.x, Cmd replaces Term.info/Term.eval entirely.', 455 steps: [ 456 { code: 'Cmdliner.Cmd.info "greet" ~doc:"Greet someone";;', 457 expect: 'Cmdliner.Cmd.info', description: 'Create command info' }, 458 { code: 'let t = Cmdliner.Term.(const (fun _ _ -> ()) $ verbose $ name);;', 459 expect: 'Cmdliner.Term.t', description: 'Build the term' }, 460 { code: 'let cmd = Cmdliner.Cmd.v (Cmdliner.Cmd.info "greet") t;;', 461 expect: 'Cmdliner.Cmd.t', description: 'Package into a command' }, 462 { code: 'Cmdliner.Cmd.name cmd;;', expect: '"greet"', 463 description: 'Extract the command name' }, 464 ] }, 465 { title: 'Removed APIs', 466 description: 'Term.eval was removed in 2.0. Use Cmd.eval_value instead.', 467 steps: [ 468 { code: 'Cmdliner.Cmd.eval_value;;', expect: 'eval_ok', 469 description: 'Cmd.eval_value is the new entry point' }, 470 ] }, 471 ], 472 }, 473 474 'cmdliner.2.1.0': { 475 name: 'Cmdliner', version: '2.1.0', opam: 'cmdliner', 476 description: 'Declarative definition of command line interfaces (v2 API)', 477 universe: U['cmdliner.2.1.0'], require: ['cmdliner'], 478 sections: [ 479 { title: 'Arguments', 480 description: 'Define typed command-line arguments with converters and info.', 481 steps: [ 482 { code: 'let verbose = Cmdliner.Arg.(value (flag (info ["v";"verbose"] ~doc:"Increase verbosity")));;', 483 expect: 'bool Cmdliner.Term.t', description: 'A verbose flag' }, 484 { code: 'let file = Cmdliner.Arg.(required (pos 0 (some string) None (info [] ~docv:"FILE")));;', 485 expect: 'string Cmdliner.Term.t', description: 'A required positional file argument' }, 486 { code: 'let count = Cmdliner.Arg.(value (opt int 1 (info ["n";"count"] ~doc:"Repeat count")));;', 487 expect: 'int Cmdliner.Term.t', description: 'An optional integer with default' }, 488 ] }, 489 { title: 'Commands', 490 description: 'Commands combine a term with metadata. Groups can nest subcommands.', 491 steps: [ 492 { code: 'let info = Cmdliner.Cmd.info "process" ~version:"1.0" ~doc:"Process files";;', 493 expect: 'Cmdliner.Cmd.info', description: 'Command info with version' }, 494 { code: 'let t = Cmdliner.Term.(const (fun _ _ _ -> ()) $ verbose $ file $ count);;', 495 expect: 'Cmdliner.Term.t', description: 'Combine all arguments' }, 496 { code: 'let cmd = Cmdliner.Cmd.v info t;;', 497 expect: 'Cmdliner.Cmd.t', description: 'Create the command' }, 498 { code: 'Cmdliner.Cmd.name cmd;;', expect: '"process"', 499 description: 'Retrieve the command name' }, 500 ] }, 501 { title: 'Custom Converters', 502 description: 'Arg.conv creates custom argument converters from a parser/printer pair.', 503 steps: [ 504 { code: 'let color_parser s = match s with "red" -> Ok `Red | "blue" -> Ok `Blue | _ -> Error (`Msg "unknown color");;', 505 expect: 'val color_parser', description: 'Define a parser function' }, 506 { code: 'let color_pp ppf c = Format.pp_print_string ppf (match c with `Red -> "red" | `Blue -> "blue");;', 507 expect: 'val color_pp', description: 'Define a printer' }, 508 { code: 'let color_conv = Cmdliner.Arg.conv (color_parser, color_pp);;', 509 expect: 'Cmdliner.Arg.conv', description: 'Build a custom converter' }, 510 ] }, 511 ], 512 }, 513 514 // ═══════════════════════════════════════════════════════════════════════ 515 // Mtime 516 // ═══════════════════════════════════════════════════════════════════════ 517 'mtime.1.3.0': mtime1_3Tutorial('1.3.0', U['mtime.1.3.0']), 518 'mtime.1.4.0': mtime1_4Tutorial('1.4.0', U['mtime.1.4.0']), 519 520 'mtime.2.1.0': { 521 name: 'Mtime', version: '2.1.0', opam: 'mtime', 522 description: 'Monotonic wall-clock time for OCaml', 523 universe: U['mtime.2.1.0'], require: ['mtime'], 524 sections: [ 525 { title: 'Time Span Constants', 526 description: 'Mtime.Span provides named constants for common durations.', 527 steps: [ 528 { code: 'Mtime.Span.zero;;', expect: 'Mtime.span', 529 description: 'Zero-length span' }, 530 { code: 'Mtime.Span.s;;', expect: 'Mtime.span', 531 description: '1 second' }, 532 { code: 'Mtime.Span.min;;', expect: 'Mtime.span', 533 description: '1 minute' }, 534 { code: 'Mtime.Span.hour;;', expect: 'Mtime.span', 535 description: '1 hour' }, 536 ] }, 537 { title: 'Span Arithmetic', 538 description: 'Spans support addition, comparison, and predicate-based comparisons (new in 2.x).', 539 steps: [ 540 { code: 'let two_sec = Mtime.Span.add Mtime.Span.s Mtime.Span.s;;', expect: 'Mtime.span', 541 description: '1s + 1s' }, 542 { code: 'Mtime.Span.to_uint64_ns two_sec;;', expect: '2000000000L', 543 description: '2 seconds in nanoseconds' }, 544 { code: 'Mtime.Span.is_shorter Mtime.Span.ms ~than:Mtime.Span.s;;', expect: 'true', 545 description: '1ms is shorter than 1s (new in 2.x)' }, 546 { code: 'Mtime.Span.is_longer Mtime.Span.hour ~than:Mtime.Span.min;;', expect: 'true', 547 description: '1 hour is longer than 1 minute (new in 2.x)' }, 548 ] }, 549 { title: 'New in 2.x: Float Conversions', 550 description: 'Mtime 2.x adds Span.of_float_ns for creating spans from floating-point nanoseconds.', 551 steps: [ 552 { code: 'Mtime.Span.of_float_ns 1e9;;', expect: 'Some', 553 description: '1e9 ns = 1 second' }, 554 { code: 'Mtime.Span.of_float_ns (-1.);;', expect: 'None', 555 description: 'Negative values return None' }, 556 { code: 'Mtime.Span.of_float_ns infinity;;', expect: 'None', 557 description: 'Non-finite values return None' }, 558 { code: 'Mtime.Span.to_float_ns Mtime.Span.s;;', expect: '1000000000.', 559 description: 'Convert 1 second to float nanoseconds' }, 560 ] }, 561 ], 562 }, 563 564 // ═══════════════════════════════════════════════════════════════════════ 565 // Logs 566 // ═══════════════════════════════════════════════════════════════════════ 567 'logs.0.7.0': { 568 name: 'Logs', version: '0.7.0', opam: 'logs', 569 description: 'Logging infrastructure for OCaml', 570 universe: U['logs.0.7.0'], require: ['logs'], 571 sections: [ 572 { title: 'Log Sources', 573 description: 'A Logs.Src.t identifies a log source with a name and optional documentation.', 574 steps: [ 575 { code: 'let src = Logs.Src.create "myapp" ~doc:"My application";;', expect: 'Logs.src', 576 description: 'Create a named log source' }, 577 { code: 'Logs.Src.name src;;', expect: '"myapp"', 578 description: 'Retrieve the source name' }, 579 { code: 'Logs.Src.doc src;;', expect: '"My application"', 580 description: 'Retrieve the source documentation' }, 581 ] }, 582 { title: 'Log Levels', 583 description: 'Logs has five levels: App, Error, Warning, Info, Debug. The global level controls what gets logged.', 584 steps: [ 585 { code: 'Logs.level ();;', expect: 'option', 586 description: 'Get the current global log level' }, 587 { code: 'Logs.set_level (Some Logs.Debug);;', expect: 'unit', 588 description: 'Set the global level to Debug (most verbose)' }, 589 { code: 'Logs.level ();;', expect: 'Some', 590 description: 'Verify the level was set' }, 591 ] }, 592 { title: 'Error Counting', 593 description: 'Logs tracks error and warning counts globally.', 594 steps: [ 595 { code: 'Logs.err_count ();;', expect: 'int', 596 description: 'Count of errors logged so far' }, 597 { code: 'Logs.warn_count ();;', expect: 'int', 598 description: 'Count of warnings logged so far' }, 599 ] }, 600 ], 601 }, 602 603 'logs.0.10.0': { 604 name: 'Logs', version: '0.10.0', opam: 'logs', 605 description: 'Logging infrastructure for OCaml', 606 universe: U['logs.0.10.0'], require: ['logs'], 607 sections: [ 608 { title: 'Log Sources', 609 description: 'Create and inspect named log sources.', 610 steps: [ 611 { code: 'let src = Logs.Src.create "test" ~doc:"A test source";;', expect: 'Logs.src', 612 description: 'Create a log source' }, 613 { code: 'Logs.Src.name src;;', expect: '"test"', 614 description: 'Get the source name' }, 615 { code: 'Logs.Src.doc src;;', expect: '"A test source"', 616 description: 'Get the documentation string' }, 617 { code: 'Logs.Src.list ();;', expect: 'Logs.src list', 618 description: 'List all registered sources' }, 619 ] }, 620 { title: 'Level Management', 621 description: 'Control log verbosity at the global and per-source levels.', 622 steps: [ 623 { code: 'Logs.set_level (Some Logs.Info);;', expect: 'unit', 624 description: 'Set global level to Info' }, 625 { code: 'Logs.level ();;', expect: 'Some', 626 description: 'Check the global level' }, 627 { code: 'Logs.Src.set_level src (Some Logs.Debug);;', expect: 'unit', 628 description: 'Override the level for a specific source' }, 629 { code: 'Logs.Src.level src;;', expect: 'Some', 630 description: 'Check the per-source level' }, 631 ] }, 632 { title: 'Error Tracking', 633 description: 'Logs maintains error and warning counters.', 634 steps: [ 635 { code: 'Logs.err_count ();;', expect: 'int', 636 description: 'Number of errors logged' }, 637 { code: 'Logs.warn_count ();;', expect: 'int', 638 description: 'Number of warnings logged' }, 639 ] }, 640 ], 641 }, 642 643 // ═══════════════════════════════════════════════════════════════════════ 644 // Uucp 645 // ═══════════════════════════════════════════════════════════════════════ 646 'uucp.14.0.0': uucpTutorial('14.0.0', U['uucp.14.0.0'], '14.0.0'), 647 'uucp.15.0.0': uucpTutorial('15.0.0', U['uucp.15.0.0'], '15.0.0'), 648 'uucp.16.0.0': uucpTutorial('16.0.0', U['uucp.16.0.0'], '16.0.0'), 649 'uucp.17.0.0': uucpTutorial('17.0.0', U['uucp.17.0.0'], '17.0.0'), 650 651 // ═══════════════════════════════════════════════════════════════════════ 652 // Uunf 653 // ═══════════════════════════════════════════════════════════════════════ 654 'uunf.14.0.0': uunfTutorial('14.0.0', U['uunf.14.0.0'], '14.0.0'), 655 'uunf.17.0.0': uunfTutorial('17.0.0', U['uunf.17.0.0'], '17.0.0'), 656 657 // ═══════════════════════════════════════════════════════════════════════ 658 // Astring 659 // ═══════════════════════════════════════════════════════════════════════ 660 'astring.0.8.5': { 661 name: 'Astring', version: '0.8.5', opam: 'astring', 662 description: 'Alternative String module for OCaml', 663 universe: U['astring.0.8.5'], require: ['astring'], 664 sections: [ 665 { title: 'String Splitting', 666 description: 'Astring.String provides powerful splitting functions that work with string separators.', 667 steps: [ 668 { code: 'Astring.String.cuts ~sep:"," "a,b,c";;', expect: '["a"; "b"; "c"]', 669 description: 'Split on comma' }, 670 { code: 'Astring.String.cuts ~sep:"::" "a::b::c";;', expect: '["a"; "b"; "c"]', 671 description: 'Split on multi-char separator' }, 672 { code: 'Astring.String.cut ~sep:"=" "key=value";;', expect: 'Some ("key", "value")', 673 description: 'Cut at first separator occurrence' }, 674 { code: 'Astring.String.cut ~rev:true ~sep:"." "a.b.c";;', expect: 'Some ("a.b", "c")', 675 description: 'Cut at last separator with ~rev:true' }, 676 ] }, 677 { title: 'String Building', 678 description: 'Concatenation and transformation functions.', 679 steps: [ 680 { code: 'Astring.String.concat ~sep:"-" ["x"; "y"; "z"];;', expect: '"x-y-z"', 681 description: 'Join strings with separator' }, 682 { code: 'Astring.String.concat ~sep:", " ["hello"; "world"];;', expect: '"hello, world"', 683 description: 'Join with comma-space' }, 684 ] }, 685 { title: 'String Testing', 686 description: 'Predicate functions for string content.', 687 steps: [ 688 { code: 'Astring.String.is_prefix ~affix:"http" "http://example.com";;', expect: 'true', 689 description: 'Check for a prefix' }, 690 { code: 'Astring.String.is_suffix ~affix:".ml" "main.ml";;', expect: 'true', 691 description: 'Check for a suffix' }, 692 { code: 'Astring.String.is_prefix ~affix:"ftp" "http://example.com";;', expect: 'false', 693 description: 'Prefix not found' }, 694 { code: 'Astring.String.find_sub ~sub:"world" "hello world";;', expect: 'Some 6', 695 description: 'Find substring position' }, 696 ] }, 697 { title: 'String Trimming', 698 description: 'Remove whitespace or specific characters from strings.', 699 steps: [ 700 { code: 'Astring.String.trim " hello ";;', expect: '"hello"', 701 description: 'Trim whitespace from both ends' }, 702 { code: 'Astring.String.trim ~drop:(fun c -> c = \'/\') "/path/to/";;', expect: '"path/to"', 703 description: 'Trim custom characters' }, 704 ] }, 705 { title: 'Substrings', 706 description: 'Astring.String.Sub provides zero-copy substring operations.', 707 steps: [ 708 { code: 'Astring.String.Sub.(to_string (v "hello world" ~start:6));;', expect: '"world"', 709 description: 'Extract a substring from position 6' }, 710 { code: 'Astring.String.Sub.(to_string (v "hello world" ~stop:5));;', expect: '"hello"', 711 description: 'Extract first 5 characters' }, 712 ] }, 713 ], 714 }, 715 716 // ═══════════════════════════════════════════════════════════════════════ 717 // Jsonm 718 // ═══════════════════════════════════════════════════════════════════════ 719 'jsonm.1.0.2': { 720 name: 'Jsonm', version: '1.0.2', opam: 'jsonm', 721 description: 'Non-blocking streaming JSON codec for OCaml', 722 universe: U['jsonm.1.0.2'], require: ['jsonm'], 723 sections: [ 724 { title: 'Decoding JSON Values', 725 description: 'Jsonm.decoder creates a streaming decoder. Each Jsonm.decode call returns one lexeme.', 726 steps: [ 727 { code: 'let d = Jsonm.decoder (`String "42");;', expect: 'Jsonm.decoder', 728 description: 'Create a decoder from a JSON string' }, 729 { code: 'Jsonm.decode d;;', expect: '`Lexeme (`Float 42.)', 730 description: 'Decode the number 42 (JSON numbers are floats)' }, 731 { code: 'Jsonm.decode d;;', expect: '`End', 732 description: 'End of input' }, 733 ] }, 734 { title: 'Decoding Strings and Booleans', 735 description: 'JSON strings, booleans, and null each produce a single lexeme.', 736 steps: [ 737 { code: 'let d2 = Jsonm.decoder (`String {|"hello"|});;', expect: 'Jsonm.decoder', 738 description: 'Decode a JSON string' }, 739 { code: 'Jsonm.decode d2;;', expect: '`Lexeme (`String "hello")', 740 description: 'String lexeme' }, 741 { code: 'let d3 = Jsonm.decoder (`String "true");;', expect: 'Jsonm.decoder', 742 description: 'Decode a JSON boolean' }, 743 { code: 'Jsonm.decode d3;;', expect: '`Lexeme (`Bool true)', 744 description: 'Boolean lexeme' }, 745 { code: 'let dn = Jsonm.decoder (`String "null");;', expect: 'Jsonm.decoder', 746 description: 'Decode null' }, 747 { code: 'Jsonm.decode dn;;', expect: '`Lexeme `Null', 748 description: 'Null lexeme' }, 749 ] }, 750 { title: 'Decoding Arrays', 751 description: 'Arrays produce `As (array start) and `Ae (array end) lexemes around their elements.', 752 steps: [ 753 { code: 'let da = Jsonm.decoder (`String "[1, 2, 3]");;', expect: 'Jsonm.decoder', 754 description: 'Create decoder for a JSON array' }, 755 { code: 'Jsonm.decode da;;', expect: '`Lexeme `As', 756 description: 'Array start' }, 757 { code: 'Jsonm.decode da;;', expect: '`Lexeme (`Float 1.)', 758 description: 'First element' }, 759 { code: 'Jsonm.decode da;;', expect: '`Lexeme (`Float 2.)', 760 description: 'Second element' }, 761 ] }, 762 { title: 'Encoding JSON', 763 description: 'Jsonm.encoder creates an encoder that writes lexemes to a buffer.', 764 steps: [ 765 { code: 'let buf = Buffer.create 64;;', expect: 'Buffer.t', 766 description: 'Create an output buffer' }, 767 { code: 'let e = Jsonm.encoder (`Buffer buf);;', expect: 'Jsonm.encoder', 768 description: 'Create an encoder' }, 769 { code: 'Jsonm.encode e (`Lexeme (`Float 42.));;', expect: '`Ok', 770 description: 'Encode a number' }, 771 { code: 'Jsonm.encode e `End;;', expect: '`Ok', 772 description: 'End encoding' }, 773 { code: 'Buffer.contents buf;;', expect: '42', 774 description: 'The buffer contains the JSON output' }, 775 ] }, 776 ], 777 }, 778 779 // ═══════════════════════════════════════════════════════════════════════ 780 // Xmlm 781 // ═══════════════════════════════════════════════════════════════════════ 782 'xmlm.1.4.0': { 783 name: 'Xmlm', version: '1.4.0', opam: 'xmlm', 784 description: 'Streaming XML codec for OCaml', 785 universe: U['xmlm.1.4.0'], require: ['xmlm'], 786 sections: [ 787 { title: 'Parsing XML Input', 788 description: 'Xmlm.make_input creates a streaming parser. Each Xmlm.input call returns one signal.', 789 steps: [ 790 { code: 'let i = Xmlm.make_input (`String (0, "<root/>"));;', expect: 'Xmlm.input', 791 description: 'Create an input from a string' }, 792 { code: 'Xmlm.input i;;', expect: '`Dtd', 793 description: 'First signal is the DTD (None for no doctype)' }, 794 { code: 'Xmlm.input i;;', expect: '`El_start', 795 description: 'Element start: <root>' }, 796 { code: 'Xmlm.input i;;', expect: '`El_end', 797 description: 'Element end: </root> (self-closing)' }, 798 ] }, 799 { title: 'Parsing with Attributes', 800 description: 'Element start signals include the tag name and attributes.', 801 steps: [ 802 { code: 'let i2 = Xmlm.make_input (`String (0, {|<div class="main">text</div>|}));;', 803 expect: 'Xmlm.input', description: 'Parse XML with attributes' }, 804 { code: 'Xmlm.input i2;;', expect: '`Dtd', 805 description: 'DTD signal' }, 806 { code: 'Xmlm.input i2;;', expect: '`El_start', 807 description: 'Element start with attributes' }, 808 { code: 'Xmlm.input i2;;', expect: '`Data "text"', 809 description: 'Text content' }, 810 { code: 'Xmlm.input i2;;', expect: '`El_end', 811 description: 'Element end' }, 812 ] }, 813 { title: 'XML Output', 814 description: 'Xmlm can also write XML to a buffer.', 815 steps: [ 816 { code: 'let buf = Buffer.create 64;;', expect: 'Buffer.t', 817 description: 'Create output buffer' }, 818 { code: 'let o = Xmlm.make_output (`Buffer buf);;', expect: 'Xmlm.output', 819 description: 'Create an XML output' }, 820 { code: 'Xmlm.output o (`Dtd None);;', expect: 'unit', 821 description: 'Write empty DTD' }, 822 { code: 'Xmlm.output o (`El_start (("", "item"), []));;', expect: 'unit', 823 description: 'Start <item> element' }, 824 { code: 'Xmlm.output o (`Data "hello");;', expect: 'unit', 825 description: 'Write text content' }, 826 { code: 'Xmlm.output o `El_end;;', expect: 'unit', 827 description: 'Close the element' }, 828 { code: 'Buffer.contents buf;;', expect: '<item>hello</item>', 829 description: 'The output XML' }, 830 ] }, 831 ], 832 }, 833 834 // ═══════════════════════════════════════════════════════════════════════ 835 // Ptime 836 // ═══════════════════════════════════════════════════════════════════════ 837 'ptime.1.2.0': { 838 name: 'Ptime', version: '1.2.0', opam: 'ptime', 839 description: 'POSIX time for OCaml', 840 universe: U['ptime.1.2.0'], require: ['ptime'], 841 sections: [ 842 { title: 'The Epoch', 843 description: 'Ptime.epoch represents 1970-01-01 00:00:00 UTC, the Unix epoch.', 844 steps: [ 845 { code: 'Ptime.epoch;;', expect: 'Ptime.t', 846 description: 'The epoch timestamp' }, 847 { code: 'Ptime.to_float_s Ptime.epoch;;', expect: '0.', 848 description: 'Epoch as float seconds = 0' }, 849 ] }, 850 { title: 'Creating Timestamps', 851 description: 'Ptime.of_date_time creates a timestamp from a date-time tuple.', 852 steps: [ 853 { code: 'let t = Ptime.of_date_time ((2024, 1, 1), ((12, 0, 0), 0));;', expect: 'Some', 854 description: 'January 1, 2024, noon UTC' }, 855 { code: 'let t = match t with Some t -> t | None -> assert false;;', expect: 'Ptime.t', 856 description: 'Unwrap the option' }, 857 { code: 'Ptime.to_date_time t;;', expect: '(2024, 1, 1)', 858 description: 'Convert back to date-time tuple' }, 859 { code: 'Ptime.to_rfc3339 t;;', expect: '2024-01-01', 860 description: 'Format as RFC 3339 string' }, 861 ] }, 862 { title: 'Time Arithmetic', 863 description: 'Add and subtract time spans from timestamps.', 864 steps: [ 865 { code: 'let one_day = Ptime.Span.of_int_s (24 * 3600);;', expect: 'Ptime.span', 866 description: 'A span of one day (86400 seconds)' }, 867 { code: 'let tomorrow = Ptime.add_span t one_day;;', expect: 'Some', 868 description: 'Add one day to our timestamp' }, 869 { code: 'Ptime.to_rfc3339 (Option.get tomorrow);;', expect: '2024-01-02', 870 description: 'January 2nd' }, 871 { code: 'Ptime.Span.to_int_s (Ptime.diff (Option.get tomorrow) t);;', expect: 'Some 86400', 872 description: 'Difference is exactly 86400 seconds' }, 873 ] }, 874 { title: 'Time Spans', 875 description: 'Ptime.Span represents durations in days and picoseconds.', 876 steps: [ 877 { code: 'Ptime.Span.zero;;', expect: 'Ptime.span', 878 description: 'Zero duration' }, 879 { code: 'Ptime.Span.of_int_s 3600;;', expect: 'Ptime.span', 880 description: 'One hour in seconds' }, 881 { code: 'Ptime.Span.to_int_s (Ptime.Span.of_int_s 3600);;', expect: 'Some 3600', 882 description: 'Round-trip: int -> span -> int' }, 883 { code: 'Ptime.Span.to_float_s (Ptime.Span.of_int_s 90);;', expect: '90.', 884 description: '90 seconds as a float' }, 885 ] }, 886 ], 887 }, 888 889 // ═══════════════════════════════════════════════════════════════════════ 890 // React 891 // ═══════════════════════════════════════════════════════════════════════ 892 'react.1.2.2': { 893 name: 'React', version: '1.2.2', opam: 'react', 894 description: 'Declarative events and signals for OCaml (FRP)', 895 universe: U['react.1.2.2'], require: ['react'], 896 sections: [ 897 { title: 'Creating Signals', 898 description: 'React.S.create returns a signal and a setter function. Signals always have a current value.', 899 steps: [ 900 { code: 'let counter, set_counter = React.S.create 0;;', expect: 'React.signal', 901 description: 'Create a signal with initial value 0' }, 902 { code: 'React.S.value counter;;', expect: '0', 903 description: 'Read the current value' }, 904 { code: 'set_counter 42;;', expect: 'unit', 905 description: 'Update the signal value' }, 906 { code: 'React.S.value counter;;', expect: '42', 907 description: 'The value has changed' }, 908 ] }, 909 { title: 'Derived Signals', 910 description: 'React.S.map creates a signal that automatically updates when its source changes.', 911 steps: [ 912 { code: 'let doubled = React.S.map (fun x -> x * 2) counter;;', expect: 'React.signal', 913 description: 'A signal that is always 2x the counter' }, 914 { code: 'React.S.value doubled;;', expect: '84', 915 description: '42 * 2 = 84' }, 916 { code: 'set_counter 10;;', expect: 'unit', 917 description: 'Update the counter' }, 918 { code: 'React.S.value doubled;;', expect: '20', 919 description: 'Doubled automatically updates: 10 * 2 = 20' }, 920 ] }, 921 { title: 'Combining Signals', 922 description: 'React.S.l2 combines two signals with a function. React.S.pair creates a signal of pairs.', 923 steps: [ 924 { code: 'let name, set_name = React.S.create "world";;', expect: 'React.signal', 925 description: 'A name signal' }, 926 { code: 'let greeting = React.S.l2 (fun n c -> Printf.sprintf "Hello %s (count=%d)" n c) name counter;;', 927 expect: 'React.signal', description: 'Combine name and counter' }, 928 { code: 'React.S.value greeting;;', expect: '"Hello world (count=10)"', 929 description: 'The combined value' }, 930 { code: 'set_name "OCaml";;', expect: 'unit', 931 description: 'Update the name' }, 932 { code: 'React.S.value greeting;;', expect: '"Hello OCaml (count=10)"', 933 description: 'Greeting updates automatically' }, 934 ] }, 935 { title: 'Events', 936 description: 'React.E.create returns an event and a trigger. Unlike signals, events are discrete occurrences.', 937 steps: [ 938 { code: 'let clicks, send_click = React.E.create ();;', expect: 'React.event', 939 description: 'Create a click event' }, 940 { code: 'let click_count = React.S.hold 0 (React.E.map (fun _ -> React.S.value counter) clicks);;', 941 expect: 'React.signal', description: 'Hold the counter value at each click' }, 942 ] }, 943 ], 944 }, 945 946 // ═══════════════════════════════════════════════════════════════════════ 947 // Hmap 948 // ═══════════════════════════════════════════════════════════════════════ 949 'hmap.0.8.1': { 950 name: 'Hmap', version: '0.8.1', opam: 'hmap', 951 description: 'Heterogeneous value maps for OCaml', 952 universe: U['hmap.0.8.1'], require: ['hmap'], 953 sections: [ 954 { title: 'Creating Keys', 955 description: 'Hmap keys are created with Key.create. Each key carries a type witness, allowing the map to hold values of different types.', 956 steps: [ 957 { code: 'let k_int : int Hmap.key = Hmap.Key.create ();;', expect: 'Hmap.key', 958 description: 'A key for int values' }, 959 { code: 'let k_str : string Hmap.key = Hmap.Key.create ();;', expect: 'Hmap.key', 960 description: 'A key for string values' }, 961 { code: 'let k_list : int list Hmap.key = Hmap.Key.create ();;', expect: 'Hmap.key', 962 description: 'A key for int list values' }, 963 ] }, 964 { title: 'Building Maps', 965 description: 'Start from Hmap.empty and add values with Hmap.add. Each key-value pair is type-safe.', 966 steps: [ 967 { code: 'let m = Hmap.empty;;', expect: 'Hmap.t', 968 description: 'An empty heterogeneous map' }, 969 { code: 'Hmap.is_empty m;;', expect: 'true', 970 description: 'Verify it is empty' }, 971 { code: 'let m = m |> Hmap.add k_int 42 |> Hmap.add k_str "hello" |> Hmap.add k_list [1;2;3];;', 972 expect: 'Hmap.t', description: 'Add values of different types' }, 973 { code: 'Hmap.cardinal m;;', expect: '3', 974 description: 'Three bindings in the map' }, 975 ] }, 976 { title: 'Querying Maps', 977 description: 'Hmap.find returns an option. The return type matches the key\'s type parameter.', 978 steps: [ 979 { code: 'Hmap.find k_int m;;', expect: 'Some 42', 980 description: 'Find the int value — type-safe!' }, 981 { code: 'Hmap.find k_str m;;', expect: 'Some "hello"', 982 description: 'Find the string value' }, 983 { code: 'Hmap.find k_list m;;', expect: 'Some [1; 2; 3]', 984 description: 'Find the int list value' }, 985 { code: 'Hmap.mem k_int m;;', expect: 'true', 986 description: 'Check membership' }, 987 { code: 'let m2 = Hmap.rem k_int m;;', expect: 'Hmap.t', 988 description: 'Remove a binding' }, 989 { code: 'Hmap.find k_int m2;;', expect: 'None', 990 description: 'Key is no longer bound' }, 991 ] }, 992 ], 993 }, 994 995 // ═══════════════════════════════════════════════════════════════════════ 996 // Gg 997 // ═══════════════════════════════════════════════════════════════════════ 998 'gg.1.0.0': { 999 name: 'Gg', version: '1.0.0', opam: 'gg', 1000 description: 'Basic types for computer graphics in OCaml', 1001 universe: U['gg.1.0.0'], require: ['gg'], 1002 sections: [ 1003 { title: '2D Vectors', 1004 description: 'Gg.V2 provides 2D vector operations. Vectors are immutable float pairs.', 1005 steps: [ 1006 { code: 'let v = Gg.V2.v 3.0 4.0;;', expect: 'Gg.v2', 1007 description: 'Create a 2D vector (3, 4)' }, 1008 { code: 'Gg.V2.x v;;', expect: '3.', 1009 description: 'X component' }, 1010 { code: 'Gg.V2.y v;;', expect: '4.', 1011 description: 'Y component' }, 1012 { code: 'Gg.V2.norm v;;', expect: '5.', 1013 description: 'Vector magnitude: sqrt(9 + 16) = 5' }, 1014 ] }, 1015 { title: 'Vector Arithmetic', 1016 description: 'Vectors support addition, subtraction, scalar multiplication, and dot products.', 1017 steps: [ 1018 { code: 'let a = Gg.V2.v 1.0 0.0;;', expect: 'Gg.v2', 1019 description: 'Unit vector along x-axis' }, 1020 { code: 'let b = Gg.V2.v 0.0 1.0;;', expect: 'Gg.v2', 1021 description: 'Unit vector along y-axis' }, 1022 { code: 'Gg.V2.add a b |> Gg.V2.x;;', expect: '1.', 1023 description: 'Addition: (1,0) + (0,1) → x = 1' }, 1024 { code: 'Gg.V2.dot a b;;', expect: '0.', 1025 description: 'Dot product of perpendicular vectors = 0' }, 1026 { code: 'Gg.V2.smul 3.0 a |> Gg.V2.x;;', expect: '3.', 1027 description: 'Scalar multiply: 3 * (1,0) → x = 3' }, 1028 ] }, 1029 { title: '3D Vectors and Cross Product', 1030 description: 'Gg.V3 adds a third dimension and the cross product operation.', 1031 steps: [ 1032 { code: 'let i = Gg.V3.v 1.0 0.0 0.0;;', expect: 'Gg.v3', 1033 description: 'X-axis unit vector' }, 1034 { code: 'let j = Gg.V3.v 0.0 1.0 0.0;;', expect: 'Gg.v3', 1035 description: 'Y-axis unit vector' }, 1036 { code: 'let k = Gg.V3.cross i j;;', expect: 'Gg.v3', 1037 description: 'Cross product i × j = k (z-axis)' }, 1038 { code: 'Gg.V3.z k;;', expect: '1.', 1039 description: 'Z component is 1 (right-hand rule)' }, 1040 ] }, 1041 { title: 'Colors', 1042 description: 'Gg.Color represents colors in linear sRGB with alpha.', 1043 steps: [ 1044 { code: 'Gg.Color.red;;', expect: 'Gg.color', 1045 description: 'Predefined red color' }, 1046 { code: 'Gg.Color.r Gg.Color.red;;', expect: '1.', 1047 description: 'Red component = 1.0' }, 1048 { code: 'Gg.Color.g Gg.Color.red;;', expect: '0.', 1049 description: 'Green component = 0.0' }, 1050 { code: 'let c = Gg.Color.v 0.5 0.8 0.2 1.0;;', expect: 'Gg.color', 1051 description: 'Create a custom RGBA color' }, 1052 { code: 'Gg.Color.a c;;', expect: '1.', 1053 description: 'Alpha component' }, 1054 ] }, 1055 ], 1056 }, 1057 1058 // ═══════════════════════════════════════════════════════════════════════ 1059 // Vg 1060 // ═══════════════════════════════════════════════════════════════════════ 1061 'vg.0.9.5': { 1062 name: 'Vg', version: '0.9.5', opam: 'vg', 1063 description: 'Declarative 2D vector graphics for OCaml', 1064 universe: U['vg.0.9.5'], require: ['vg', 'gg'], 1065 sections: [ 1066 { title: 'Building Paths', 1067 description: 'Vg.P builds immutable path values by chaining operations on P.empty.', 1068 steps: [ 1069 { code: 'let p = Vg.P.empty;;', expect: 'Vg.path', 1070 description: 'Start with an empty path' }, 1071 { code: 'let p = Vg.P.empty |> Vg.P.sub (Gg.P2.v 0. 0.) |> Vg.P.line (Gg.P2.v 1. 1.);;', 1072 expect: 'Vg.path', description: 'A line from (0,0) to (1,1)' }, 1073 { code: 'let circ = Vg.P.empty |> Vg.P.circle (Gg.P2.v 0.5 0.5) 0.3;;', 1074 expect: 'Vg.path', description: 'A circle centered at (0.5, 0.5) with radius 0.3' }, 1075 { code: 'let rect = Vg.P.empty |> Vg.P.rect (Gg.Box2.v (Gg.P2.v 0. 0.) (Gg.Size2.v 1. 1.));;', 1076 expect: 'Vg.path', description: 'A unit rectangle' }, 1077 ] }, 1078 { title: 'Creating Images', 1079 description: 'Vg.I constructs images from colors, paths, and compositing operations.', 1080 steps: [ 1081 { code: 'let red_fill = Vg.I.const Gg.Color.red;;', expect: 'Vg.image', 1082 description: 'A solid red infinite image' }, 1083 { code: 'let red_circle = Vg.I.cut circ red_fill;;', expect: 'Vg.image', 1084 description: 'Cut the red fill to the circle path' }, 1085 { code: 'let blue_rect = Vg.I.cut rect (Vg.I.const Gg.Color.blue);;', expect: 'Vg.image', 1086 description: 'A blue rectangle' }, 1087 ] }, 1088 { title: 'Compositing Images', 1089 description: 'Vg.I.blend composites images. I.tr applies affine transforms via Gg.M3 matrices.', 1090 steps: [ 1091 { code: 'let scene = Vg.I.blend red_circle blue_rect;;', expect: 'Vg.image', 1092 description: 'Blend circle over rectangle' }, 1093 { code: 'Vg.I.void;;', expect: 'Vg.image', 1094 description: 'The empty (transparent) image' }, 1095 { code: 'let moved = Vg.I.move (Gg.V2.v 0.5 0.5) red_circle;;', expect: 'Vg.image', 1096 description: 'Translate the circle by (0.5, 0.5)' }, 1097 ] }, 1098 ], 1099 }, 1100 1101 // ═══════════════════════════════════════════════════════════════════════ 1102 // Note 1103 // ═══════════════════════════════════════════════════════════════════════ 1104 'note.0.0.3': { 1105 name: 'Note', version: '0.0.3', opam: 'note', 1106 description: 'Declarative events and signals for OCaml', 1107 universe: U['note.0.0.3'], require: ['note'], 1108 sections: [ 1109 { title: 'Constant Signals', 1110 description: 'Note.S.const creates a signal with a fixed value. Signals always have a current value.', 1111 steps: [ 1112 { code: 'let s = Note.S.const 42;;', expect: 'Note.signal', 1113 description: 'A constant signal with value 42' }, 1114 { code: 'Note.S.value s;;', expect: '42', 1115 description: 'Read the signal value' }, 1116 ] }, 1117 { title: 'Mutable Signals', 1118 description: 'Note.S.create returns a signal and a setter function for updating the value.', 1119 steps: [ 1120 { code: 'let counter, set_counter = Note.S.create 0;;', expect: 'Note.signal', 1121 description: 'Create a mutable signal starting at 0' }, 1122 { code: 'Note.S.value counter;;', expect: '0', 1123 description: 'Initial value' }, 1124 { code: 'set_counter 10;;', expect: 'unit', 1125 description: 'Update the value to 10' }, 1126 { code: 'Note.S.value counter;;', expect: '10', 1127 description: 'Value has changed' }, 1128 ] }, 1129 { title: 'Signal Transformations', 1130 description: 'Note.S.map and Note.S.l2 derive new signals from existing ones.', 1131 steps: [ 1132 { code: 'let doubled = Note.S.map (( * ) 2) counter;;', expect: 'Note.signal', 1133 description: 'A derived signal: always 2x the counter' }, 1134 { code: 'Note.S.value doubled;;', expect: '20', 1135 description: '10 * 2 = 20' }, 1136 { code: 'let label = Note.S.map (fun n -> Printf.sprintf "count=%d" n) counter;;', 1137 expect: 'Note.signal', description: 'Map counter to a string label' }, 1138 { code: 'Note.S.value label;;', expect: '"count=10"', 1139 description: 'Label reflects the current counter value' }, 1140 { code: 'let sum = Note.S.l2 ( + ) counter doubled;;', expect: 'Note.signal', 1141 description: 'Combine two signals with l2' }, 1142 { code: 'Note.S.value sum;;', expect: '30', 1143 description: '10 + 20 = 30' }, 1144 ] }, 1145 ], 1146 }, 1147 1148 // ═══════════════════════════════════════════════════════════════════════ 1149 // Otfm 1150 // ═══════════════════════════════════════════════════════════════════════ 1151 'otfm.0.4.0': { 1152 name: 'Otfm', version: '0.4.0', opam: 'otfm', 1153 description: 'OpenType font decoder for OCaml', 1154 universe: U['otfm.0.4.0'], require: ['otfm'], 1155 sections: [ 1156 { title: 'Decoder Creation', 1157 description: 'Otfm.decoder creates a decoder from font byte data. Most operations require valid font data.', 1158 steps: [ 1159 { code: 'Otfm.decoder;;', expect: '-> Otfm.decoder', 1160 description: 'The decoder constructor (takes a `String source)' }, 1161 { code: 'let d = Otfm.decoder (`String "");;', expect: 'Otfm.decoder', 1162 description: 'Create a decoder (with empty data for exploration)' }, 1163 ] }, 1164 { title: 'Querying Font Data', 1165 description: 'With valid font data, you can query tables, glyph counts, and PostScript names.', 1166 steps: [ 1167 { code: 'Otfm.flavour d;;', expect: 'Error', 1168 description: 'Flavour fails on empty data (expected)' }, 1169 { code: 'Otfm.postscript_name d;;', expect: '', 1170 description: 'PostScript name query (fails gracefully on empty data)' }, 1171 { code: 'Otfm.glyph_count d;;', expect: '', 1172 description: 'Glyph count query' }, 1173 ] }, 1174 ], 1175 }, 1176 1177 // ═══════════════════════════════════════════════════════════════════════ 1178 // Fpath 1179 // ═══════════════════════════════════════════════════════════════════════ 1180 'fpath.0.7.3': { 1181 name: 'Fpath', version: '0.7.3', opam: 'fpath', 1182 description: 'File system paths for OCaml', 1183 universe: U['fpath.0.7.3'], require: ['fpath'], 1184 sections: [ 1185 { title: 'Creating Paths', 1186 description: 'Fpath.v creates a path from a string. Paths are validated on creation.', 1187 steps: [ 1188 { code: 'Fpath.v "/usr/local/bin";;', expect: 'Fpath.t', 1189 description: 'Create an absolute path' }, 1190 { code: 'Fpath.v "/usr/local/bin" |> Fpath.to_string;;', expect: '"/usr/local/bin"', 1191 description: 'Convert back to string' }, 1192 { code: 'Fpath.v "src/main.ml";;', expect: 'Fpath.t', 1193 description: 'A relative path' }, 1194 ] }, 1195 { title: 'Path Composition', 1196 description: 'Fpath.(/) appends a segment. Paths compose naturally.', 1197 steps: [ 1198 { code: 'Fpath.(v "/usr" / "local" / "bin") |> Fpath.to_string;;', expect: '"/usr/local/bin"', 1199 description: 'Build paths by appending segments' }, 1200 { code: 'Fpath.(v "src" / "lib" / "main.ml") |> Fpath.to_string;;', expect: '"src/lib/main.ml"', 1201 description: 'Relative path composition' }, 1202 ] }, 1203 { title: 'Path Components', 1204 description: 'Extract parts of a path: parent directory, basename, filename.', 1205 steps: [ 1206 { code: 'Fpath.v "/usr/local/bin" |> Fpath.parent |> Fpath.to_string;;', expect: '"/usr/local/"', 1207 description: 'Parent directory' }, 1208 { code: 'Fpath.v "/usr/local/bin" |> Fpath.basename;;', expect: '"bin"', 1209 description: 'Basename (last segment)' }, 1210 { code: 'Fpath.v "/usr/local/bin" |> Fpath.filename;;', expect: '"bin"', 1211 description: 'Filename (last non-empty segment)' }, 1212 { code: 'Fpath.v "/a/b/" |> Fpath.basename;;', expect: '"b"', 1213 description: 'Basename of a directory path' }, 1214 { code: 'Fpath.segs (Fpath.v "/a/b/c");;', expect: '[""; "a"; "b"; "c"]', 1215 description: 'All segments (empty first = absolute)' }, 1216 ] }, 1217 { title: 'File Extensions', 1218 description: 'Query and manipulate file extensions.', 1219 steps: [ 1220 { code: 'Fpath.has_ext ".ml" (Fpath.v "main.ml");;', expect: 'true', 1221 description: 'Check for .ml extension' }, 1222 { code: 'Fpath.get_ext (Fpath.v "archive.tar.gz");;', expect: '".gz"', 1223 description: 'Get the last extension' }, 1224 { code: 'Fpath.get_ext ~multi:true (Fpath.v "archive.tar.gz");;', expect: '".tar.gz"', 1225 description: 'Get the full multi-extension' }, 1226 { code: 'Fpath.rem_ext (Fpath.v "main.ml") |> Fpath.to_string;;', expect: '"main"', 1227 description: 'Remove the extension' }, 1228 ] }, 1229 { title: 'Path Properties', 1230 description: 'Test whether paths are absolute, relative, file paths, or directory paths.', 1231 steps: [ 1232 { code: 'Fpath.is_abs (Fpath.v "/usr/bin");;', expect: 'true', 1233 description: 'Absolute path check' }, 1234 { code: 'Fpath.is_rel (Fpath.v "src/main.ml");;', expect: 'true', 1235 description: 'Relative path check' }, 1236 { code: 'Fpath.is_dir_path (Fpath.v "/usr/bin/");;', expect: 'true', 1237 description: 'Directory path (ends with /)' }, 1238 { code: 'Fpath.is_file_path (Fpath.v "/usr/bin");;', expect: 'true', 1239 description: 'File path (does not end with /)' }, 1240 { code: 'Fpath.normalize (Fpath.v "/a/b/../c") |> Fpath.to_string;;', expect: '"/a/c"', 1241 description: 'Normalize resolves .. components' }, 1242 ] }, 1243 ], 1244 }, 1245 1246 // ═══════════════════════════════════════════════════════════════════════ 1247 // Uutf 1248 // ═══════════════════════════════════════════════════════════════════════ 1249 'uutf.1.0.4': { 1250 name: 'Uutf', version: '1.0.4', opam: 'uutf', 1251 description: 'Non-blocking streaming Unicode codec for OCaml', 1252 universe: U['uutf.1.0.4'], require: ['uutf'], 1253 sections: [ 1254 { title: 'UTF-8 Decoding', 1255 description: 'Uutf.decoder creates a streaming decoder. Each Uutf.decode call returns one character or a signal.', 1256 steps: [ 1257 { code: 'let d = Uutf.decoder ~encoding:`UTF_8 (`String "ABC");;', expect: 'Uutf.decoder', 1258 description: 'Create a UTF-8 decoder for "ABC"' }, 1259 { code: 'Uutf.decode d;;', expect: '`Uchar', 1260 description: 'Decode first character: A' }, 1261 { code: 'Uutf.decode d;;', expect: '`Uchar', 1262 description: 'Decode second character: B' }, 1263 { code: 'Uutf.decode d;;', expect: '`Uchar', 1264 description: 'Decode third character: C' }, 1265 { code: 'Uutf.decode d;;', expect: '`End', 1266 description: 'End of input' }, 1267 ] }, 1268 { title: 'Multi-byte Characters', 1269 description: 'UTF-8 encodes non-ASCII characters in multiple bytes. Uutf handles this transparently.', 1270 steps: [ 1271 { code: 'let d2 = Uutf.decoder ~encoding:`UTF_8 (`String "caf\\xC3\\xA9");;', 1272 expect: 'Uutf.decoder', description: 'Decode "cafe" with e-acute (U+00E9)' }, 1273 { code: 'Uutf.decode d2;;', expect: '`Uchar', 1274 description: 'c' }, 1275 { code: 'Uutf.decode d2;;', expect: '`Uchar', 1276 description: 'a' }, 1277 { code: 'Uutf.decode d2;;', expect: '`Uchar', 1278 description: 'f' }, 1279 { code: 'Uutf.decode d2;;', expect: '`Uchar', 1280 description: 'e-acute (U+00E9, decoded from 2 bytes)' }, 1281 ] }, 1282 { title: 'UTF-8 Encoding', 1283 description: 'Uutf.encoder writes Unicode characters to a buffer in a specified encoding.', 1284 steps: [ 1285 { code: 'let buf = Buffer.create 16;;', expect: 'Buffer.t', 1286 description: 'Create an output buffer' }, 1287 { code: 'let e = Uutf.encoder `UTF_8 (`Buffer buf);;', expect: 'Uutf.encoder', 1288 description: 'Create a UTF-8 encoder' }, 1289 { code: 'Uutf.encode e (`Uchar (Uchar.of_int 0x41));;', expect: '`Ok', 1290 description: "Encode 'A'" }, 1291 { code: 'Uutf.encode e (`Uchar (Uchar.of_int 0xE9));;', expect: '`Ok', 1292 description: "Encode e-acute" }, 1293 { code: 'Uutf.encode e `End;;', expect: '`Ok', 1294 description: 'Flush the encoder' }, 1295 { code: 'Buffer.length buf;;', expect: '3', 1296 description: "A (1 byte) + e-acute (2 bytes) = 3 bytes" }, 1297 ] }, 1298 ], 1299 }, 1300 1301 // ═══════════════════════════════════════════════════════════════════════ 1302 // B0 1303 // ═══════════════════════════════════════════════════════════════════════ 1304 'b0.0.0.6': { 1305 name: 'B0', version: '0.0.6', opam: 'b0', 1306 description: 'Software construction and deployment kit', 1307 universe: U['b0.0.0.6'], require: ['b0.std'], 1308 sections: [ 1309 { title: 'File Paths (B0_std.Fpath)', 1310 description: 'B0_std provides its own Fpath module for file path manipulation.', 1311 steps: [ 1312 { code: 'B0_std.Fpath.v "/usr/bin";;', expect: 'B0_std.Fpath.t', 1313 description: 'Create a path' }, 1314 { code: 'B0_std.Fpath.(v "/usr" / "local" / "bin") |> B0_std.Fpath.to_string;;', 1315 expect: '"/usr/local/bin"', description: 'Path composition with (/)' }, 1316 { code: 'B0_std.Fpath.basename (B0_std.Fpath.v "/usr/local/bin");;', expect: '"bin"', 1317 description: 'Get the basename' }, 1318 { code: 'B0_std.Fpath.parent (B0_std.Fpath.v "/usr/local/bin") |> B0_std.Fpath.to_string;;', 1319 expect: '"/usr/local/"', description: 'Get parent directory' }, 1320 ] }, 1321 { title: 'Command Lines (B0_std.Cmd)', 1322 description: 'B0_std.Cmd builds command-line invocations declaratively.', 1323 steps: [ 1324 { code: 'let cmd = B0_std.Cmd.(tool "ocamlfind" % "query" % "-format" % "%d" % "fmt");;', 1325 expect: 'B0_std.Cmd.t', description: 'Build a command line' }, 1326 { code: 'B0_std.Cmd.to_list cmd;;', expect: '["ocamlfind"', 1327 description: 'Convert to a list of strings' }, 1328 { code: 'B0_std.Cmd.is_empty B0_std.Cmd.empty;;', expect: 'true', 1329 description: 'Check for empty command' }, 1330 ] }, 1331 ], 1332 }, 1333 1334 // ═══════════════════════════════════════════════════════════════════════ 1335 // Bos 1336 // ═══════════════════════════════════════════════════════════════════════ 1337 // ═══════════════════════════════════════════════════════════════════════ 1338 // Yojson 1339 // ═══════════════════════════════════════════════════════════════════════ 1340 'yojson.1.7.0': { 1341 name: 'Yojson', version: '1.7.0', opam: 'yojson', 1342 description: 'JSON parsing and printing for OCaml (1.x API)', 1343 universe: U['yojson.1.7.0'], require: ['yojson'], 1344 sections: [ 1345 { title: 'Parsing JSON', 1346 description: 'Yojson.Safe.from_string parses a JSON string into an algebraic type.', 1347 steps: [ 1348 { code: 'Yojson.Safe.from_string {|{"name": "Alice", "age": 30}|};;', 1349 expect: '`Assoc', description: 'Parse a JSON object' }, 1350 { code: 'Yojson.Safe.from_string "[1, 2, 3]";;', 1351 expect: '`List', description: 'Parse a JSON array' }, 1352 { code: 'Yojson.Safe.from_string "42";;', 1353 expect: '`Int 42', description: 'Parse a JSON number' }, 1354 ] }, 1355 { title: 'Building JSON', 1356 description: 'JSON values are polymorphic variants: `Null, `Bool, `Int, `Float, `String, `List, `Assoc.', 1357 steps: [ 1358 { code: 'Yojson.Safe.to_string (`Assoc [("x", `Int 1); ("y", `Int 2)]);;', 1359 expect: '"x"', description: 'Serialize an object to string' }, 1360 { code: 'Yojson.Safe.to_string (`List [`String "a"; `Bool true]);;', 1361 expect: '"a"', description: 'Serialize a list' }, 1362 ] }, 1363 { title: 'Util Module', 1364 description: 'Yojson.Safe.Util provides accessor functions for extracting values from JSON.', 1365 steps: [ 1366 { code: 'let j = Yojson.Safe.from_string {|{"name": "Bob"}|};;', expect: '`Assoc', 1367 description: 'Parse a JSON object' }, 1368 { code: 'Yojson.Safe.Util.member "name" j;;', expect: '`String "Bob"', 1369 description: 'Extract a field by name' }, 1370 { code: 'Yojson.Safe.Util.member "name" j |> Yojson.Safe.Util.to_string;;', 1371 expect: '"Bob"', description: 'Extract as an OCaml string' }, 1372 { code: 'Yojson.Safe.Util.keys (`Assoc [("a", `Int 1); ("b", `Int 2)]);;', 1373 expect: '["a"; "b"]', description: 'Get all keys of an object' }, 1374 ] }, 1375 ], 1376 }, 1377 1378 'yojson.2.0.2': { 1379 name: 'Yojson', version: '2.0.2', opam: 'yojson', 1380 description: 'JSON parsing and printing for OCaml (2.x API)', 1381 universe: U['yojson.2.0.2'], require: ['yojson'], 1382 sections: [ 1383 { title: 'Parsing JSON', 1384 description: 'Yojson 2.x removed biniou dependency. The core API is the same.', 1385 steps: [ 1386 { code: 'Yojson.Safe.from_string {|{"key": "value"}|};;', 1387 expect: '`Assoc', description: 'Parse a JSON object' }, 1388 { code: 'Yojson.Safe.from_string "true";;', 1389 expect: '`Bool true', description: 'Parse a boolean' }, 1390 ] }, 1391 { title: 'Building and Serializing', 1392 steps: [ 1393 { code: 'Yojson.Safe.to_string (`Assoc [("n", `Int 42)]);;', 1394 expect: '"n"', description: 'Serialize to compact JSON' }, 1395 { code: 'Yojson.Safe.pretty_to_string (`Assoc [("n", `Int 42)]);;', 1396 expect: '"n"', description: 'Pretty-print with indentation' }, 1397 ] }, 1398 { title: 'Util Accessors', 1399 description: 'Extract typed values from JSON trees.', 1400 steps: [ 1401 { code: 'Yojson.Safe.Util.to_int (`Int 42);;', expect: '42', 1402 description: 'Extract an int' }, 1403 { code: 'Yojson.Safe.Util.to_list (`List [`Int 1; `Int 2]);;', 1404 expect: '[`Int 1', description: 'Extract a list' }, 1405 { code: 'Yojson.Safe.Util.to_bool (`Bool true);;', expect: 'true', 1406 description: 'Extract a bool' }, 1407 { code: 'Yojson.Safe.Util.member "x" (`Assoc [("x", `Float 3.14)]);;', 1408 expect: '`Float 3.14', description: 'Navigate into an object' }, 1409 ] }, 1410 ], 1411 }, 1412 1413 'yojson.2.1.2': { 1414 name: 'Yojson', version: '2.1.2', opam: 'yojson', 1415 description: 'JSON parsing and printing for OCaml', 1416 universe: U['yojson.2.1.2'], require: ['yojson'], 1417 sections: [ 1418 { title: 'Parsing and Serializing', 1419 steps: [ 1420 { code: 'Yojson.Safe.from_string {|[1, "two", true]|};;', 1421 expect: '`List', description: 'Parse a heterogeneous array' }, 1422 { code: 'Yojson.Safe.to_string (`List [`Int 1; `String "two"; `Bool true]);;', 1423 expect: '1', description: 'Serialize back to JSON' }, 1424 ] }, 1425 { title: 'Util Navigation', 1426 steps: [ 1427 { code: 'let data = Yojson.Safe.from_string {|{"users": [{"name": "A"}, {"name": "B"}]}|};;', 1428 expect: '`Assoc', description: 'Parse nested JSON' }, 1429 { code: 'Yojson.Safe.Util.(member "users" data |> to_list |> List.map (member "name"));;', 1430 expect: '[`String "A"', description: 'Navigate and extract nested values' }, 1431 { code: 'Yojson.Safe.Util.(member "users" data |> index 0 |> member "name" |> to_string);;', 1432 expect: '"A"', description: 'Index into arrays' }, 1433 ] }, 1434 ], 1435 }, 1436 1437 'yojson.2.2.2': { 1438 name: 'Yojson', version: '2.2.2', opam: 'yojson', 1439 description: 'JSON parsing and printing for OCaml', 1440 universe: U['yojson.2.2.2'], require: ['yojson'], 1441 sections: [ 1442 { title: 'Round-Trip JSON', 1443 steps: [ 1444 { code: 'let j = `Assoc [("list", `List [`Int 1; `Int 2; `Int 3])];;', 1445 expect: '`Assoc', description: 'Build a JSON value' }, 1446 { code: 'let s = Yojson.Safe.to_string j;;', expect: 'string', 1447 description: 'Serialize to string' }, 1448 { code: 'Yojson.Safe.from_string s = j;;', expect: 'true', 1449 description: 'Round-trip preserves structure' }, 1450 ] }, 1451 { title: 'Util Combinators', 1452 steps: [ 1453 { code: 'Yojson.Safe.Util.combine (`Assoc [("a", `Int 1)]) (`Assoc [("b", `Int 2)]);;', 1454 expect: '`Assoc', description: 'Merge two objects' }, 1455 { code: 'Yojson.Safe.Util.to_assoc (`Assoc [("x", `Int 1)]);;', 1456 expect: '[("x"', description: 'Convert to association list' }, 1457 { code: 'Yojson.Safe.Util.filter_member "name" [`Assoc [("name", `String "A")]; `Assoc []];;', 1458 expect: '[`String "A"', description: 'Filter objects by field presence' }, 1459 ] }, 1460 ], 1461 }, 1462 1463 'yojson.3.0.0': { 1464 name: 'Yojson', version: '3.0.0', opam: 'yojson', 1465 description: 'JSON parsing and printing for OCaml (3.x, strict types)', 1466 universe: U['yojson.3.0.0'], require: ['yojson'], 1467 sections: [ 1468 { title: 'Strict JSON Types', 1469 description: 'Yojson 3.0 removed non-standard Tuple and Variant constructors from Safe.t.', 1470 steps: [ 1471 { code: 'Yojson.Safe.from_string {|{"clean": true}|};;', 1472 expect: '`Assoc', description: 'Parse standard JSON' }, 1473 { code: 'Yojson.Safe.to_string (`Assoc [("v", `Intlit "999999999999999")]);;', 1474 expect: '999999999999999', description: 'Intlit preserves large integers as strings' }, 1475 ] }, 1476 { title: 'Util Module', 1477 steps: [ 1478 { code: 'Yojson.Safe.Util.member "x" (`Assoc [("x", `Null)]);;', 1479 expect: '`Null', description: 'Access a null field' }, 1480 { code: 'Yojson.Safe.Util.values (`Assoc [("a", `Int 1); ("b", `Int 2)]);;', 1481 expect: '[`Int 1', description: 'Extract all values' }, 1482 { code: 'Yojson.Safe.Util.to_string_option (`Null);;', expect: 'None', 1483 description: 'Safe accessor returns None for wrong type' }, 1484 { code: 'Yojson.Safe.Util.to_string_option (`String "hi");;', expect: 'Some "hi"', 1485 description: 'Safe accessor returns Some for correct type' }, 1486 ] }, 1487 ], 1488 }, 1489 1490 // ═══════════════════════════════════════════════════════════════════════ 1491 // Ezjsonm 1492 // ═══════════════════════════════════════════════════════════════════════ 1493 'ezjsonm.1.1.0': { 1494 name: 'Ezjsonm', version: '1.1.0', opam: 'ezjsonm', 1495 description: 'Easy JSON manipulation for OCaml', 1496 universe: U['ezjsonm.1.1.0'], require: ['ezjsonm'], 1497 sections: [ 1498 { title: 'Building Values', 1499 description: 'Ezjsonm provides typed constructors for JSON values.', 1500 steps: [ 1501 { code: 'Ezjsonm.string "hello";;', expect: '`String "hello"', 1502 description: 'Create a JSON string' }, 1503 { code: 'Ezjsonm.int 42;;', expect: '`Float 42.', 1504 description: 'Create a JSON number (stored as float internally)' }, 1505 { code: 'Ezjsonm.bool true;;', expect: '`Bool true', 1506 description: 'Create a JSON boolean' }, 1507 { code: 'Ezjsonm.list Ezjsonm.int [1; 2; 3];;', expect: '`A', 1508 description: 'Create a JSON array from a list' }, 1509 ] }, 1510 { title: 'Serialization', 1511 description: 'Convert values to and from strings.', 1512 steps: [ 1513 { code: 'Ezjsonm.value_to_string (Ezjsonm.string "hi");;', expect: 'string', 1514 description: 'Serialize a value (JSON-encoded string with quotes)' }, 1515 { code: 'Ezjsonm.value_to_string (Ezjsonm.list Ezjsonm.int [1;2;3]);;', 1516 expect: '[1', description: 'Serialize an array' }, 1517 { code: 'Ezjsonm.value_from_string "42";;', expect: '`Float 42.', 1518 description: 'Parse a JSON value from string' }, 1519 ] }, 1520 { title: 'Extracting Values', 1521 description: 'get_* functions extract OCaml values from JSON.', 1522 steps: [ 1523 { code: 'Ezjsonm.get_string (Ezjsonm.string "test");;', expect: '"test"', 1524 description: 'Extract a string' }, 1525 { code: 'Ezjsonm.get_int (Ezjsonm.int 42);;', expect: '42', 1526 description: 'Extract an int' }, 1527 { code: 'Ezjsonm.get_list Ezjsonm.get_int (Ezjsonm.list Ezjsonm.int [1;2;3]);;', 1528 expect: '[1; 2; 3]', description: 'Extract a list of ints' }, 1529 ] }, 1530 ], 1531 }, 1532 1533 'ezjsonm.1.2.0': { 1534 name: 'Ezjsonm', version: '1.2.0', opam: 'ezjsonm', 1535 description: 'Easy JSON manipulation for OCaml', 1536 universe: U['ezjsonm.1.2.0'], require: ['ezjsonm'], 1537 sections: [ 1538 { title: 'Building and Querying', 1539 steps: [ 1540 { code: 'let doc = Ezjsonm.dict [("name", Ezjsonm.string "Alice"); ("age", Ezjsonm.int 30)];;', 1541 expect: '`O', description: 'Build a JSON object with dict' }, 1542 { code: 'Ezjsonm.value_to_string doc;;', expect: 'string', 1543 description: 'Serialize the object to JSON' }, 1544 { code: 'Ezjsonm.get_dict doc;;', expect: '[("name"', 1545 description: 'Extract as association list' }, 1546 ] }, 1547 { title: 'Navigating Documents', 1548 description: 'Ezjsonm.find navigates into nested JSON using a path of string keys.', 1549 steps: [ 1550 { code: 'let j = Ezjsonm.from_string {|{"user": {"name": "Bob"}}|};;', 1551 expect: '`O', description: 'Parse a nested document' }, 1552 { code: 'Ezjsonm.find j ["user"; "name"];;', expect: '`String "Bob"', 1553 description: 'Navigate by key path' }, 1554 { code: 'Ezjsonm.mem j ["user"; "name"];;', expect: 'true', 1555 description: 'Check if a path exists' }, 1556 { code: 'Ezjsonm.find_opt j ["user"; "email"];;', expect: 'None', 1557 description: 'Safe navigation returns None for missing paths' }, 1558 ] }, 1559 ], 1560 }, 1561 1562 'ezjsonm.1.3.0': { 1563 name: 'Ezjsonm', version: '1.3.0', opam: 'ezjsonm', 1564 description: 'Easy JSON manipulation for OCaml', 1565 universe: U['ezjsonm.1.3.0'], require: ['ezjsonm'], 1566 sections: [ 1567 { title: 'Value Constructors', 1568 steps: [ 1569 { code: 'Ezjsonm.string "hello";;', expect: '`String', 1570 description: 'A JSON string value' }, 1571 { code: 'Ezjsonm.unit ();;', expect: '`Null', 1572 description: 'JSON null' }, 1573 { code: 'Ezjsonm.list Ezjsonm.string ["a"; "b"];;', expect: '`A', 1574 description: 'Array of strings' }, 1575 ] }, 1576 { title: 'Documents', 1577 description: 'Documents (Ezjsonm.t) must be arrays or objects at the top level.', 1578 steps: [ 1579 { code: 'let doc = Ezjsonm.from_string {|{"x": [1, 2, 3]}|};;', expect: '`O', 1580 description: 'Parse a document' }, 1581 { code: 'Ezjsonm.find doc ["x"] |> Ezjsonm.get_list Ezjsonm.get_int;;', 1582 expect: '[1; 2; 3]', description: 'Navigate and extract typed values' }, 1583 { code: 'Ezjsonm.to_string ~minify:false doc;;', expect: 'string', 1584 description: 'Pretty-print a document' }, 1585 ] }, 1586 ], 1587 }, 1588 1589 // ═══════════════════════════════════════════════════════════════════════ 1590 // Sexplib0 1591 // ═══════════════════════════════════════════════════════════════════════ 1592 'sexplib0.v0.15.1': { 1593 name: 'Sexplib0', version: 'v0.15.1', opam: 'sexplib0', 1594 description: 'S-expression type and printing (minimal, no parsing)', 1595 universe: U['sexplib0.v0.15.1'], require: ['sexplib0'], 1596 sections: [ 1597 { title: 'S-expression Type', 1598 description: 'Sexplib0.Sexp.t has two constructors: Atom of string and List of t list.', 1599 steps: [ 1600 { code: 'Sexplib0.Sexp.Atom "hello";;', expect: 'Sexplib0.Sexp.t', 1601 description: 'An atomic S-expression' }, 1602 { code: 'Sexplib0.Sexp.List [Atom "add"; Atom "1"; Atom "2"];;', 1603 expect: 'Sexplib0.Sexp.t', description: 'A list S-expression' }, 1604 ] }, 1605 { title: 'Printing', 1606 description: 'to_string produces compact output, to_string_hum produces indented output.', 1607 steps: [ 1608 { code: 'Sexplib0.Sexp.to_string (List [Atom "name"; Atom "Alice"]);;', 1609 expect: '"(name Alice)"', description: 'Compact string representation' }, 1610 { code: 'Sexplib0.Sexp.to_string_hum (List [Atom "config"; List [Atom "port"; Atom "8080"]]);;', 1611 expect: '(config', description: 'Human-readable indented output' }, 1612 ] }, 1613 { title: 'Comparison', 1614 steps: [ 1615 { code: 'Sexplib0.Sexp.equal (Atom "x") (Atom "x");;', expect: 'true', 1616 description: 'Structural equality' }, 1617 { code: 'Sexplib0.Sexp.equal (Atom "x") (Atom "y");;', expect: 'false', 1618 description: 'Different atoms are not equal' }, 1619 ] }, 1620 ], 1621 }, 1622 1623 'sexplib0.v0.16.0': { 1624 name: 'Sexplib0', version: 'v0.16.0', opam: 'sexplib0', 1625 description: 'S-expression type and printing (minimal, no parsing)', 1626 universe: U['sexplib0.v0.16.0'], require: ['sexplib0'], 1627 sections: [ 1628 { title: 'Building S-expressions', 1629 steps: [ 1630 { code: 'open Sexplib0.Sexp;; let s = List [Atom "person"; List [Atom "name"; Atom "Bob"]; List [Atom "age"; Atom "25"]];;', 1631 expect: 'Sexplib0.Sexp.t', description: 'Build a nested S-expression' }, 1632 { code: 'Sexplib0.Sexp.to_string s;;', expect: '"(person (name Bob) (age 25))"', 1633 description: 'Serialize to compact string' }, 1634 { code: 'Sexplib0.Sexp.to_string_hum s;;', expect: '(person', 1635 description: 'Pretty-print with indentation' }, 1636 ] }, 1637 { title: 'Error Messages', 1638 description: 'Sexp.message builds structured error S-expressions.', 1639 steps: [ 1640 { code: 'Sexplib0.Sexp.message "invalid input" ["value", Atom "42"; "expected", Atom "string"];;', 1641 expect: 'Sexplib0.Sexp.t', description: 'Build a structured error message' }, 1642 ] }, 1643 ], 1644 }, 1645 1646 'sexplib0.v0.17.0': { 1647 name: 'Sexplib0', version: 'v0.17.0', opam: 'sexplib0', 1648 description: 'S-expression type and printing (minimal, no parsing)', 1649 universe: U['sexplib0.v0.17.0'], require: ['sexplib0'], 1650 sections: [ 1651 { title: 'S-expression Basics', 1652 steps: [ 1653 { code: 'let open Sexplib0.Sexp in Atom "hello";;', expect: 'Sexplib0.Sexp.t', 1654 description: 'An atom' }, 1655 { code: 'let open Sexplib0.Sexp in List [Atom "list"; List [Atom "1"; Atom "2"; Atom "3"]];;', 1656 expect: 'Sexplib0.Sexp.t', description: 'Nested S-expression' }, 1657 { code: 'Sexplib0.Sexp.(to_string (List [Atom "a"; Atom "b"; Atom "c"]));;', 1658 expect: '"(a b c)"', description: 'Serialize to string' }, 1659 ] }, 1660 { title: 'Comparison and Equality', 1661 steps: [ 1662 { code: 'Sexplib0.Sexp.compare (Atom "a") (Atom "b");;', expect: '-1', 1663 description: 'Lexicographic comparison' }, 1664 { code: 'Sexplib0.Sexp.equal (List [Atom "x"]) (List [Atom "x"]);;', expect: 'true', 1665 description: 'Deep structural equality' }, 1666 ] }, 1667 ], 1668 }, 1669 1670 // ═══════════════════════════════════════════════════════════════════════ 1671 // Csexp 1672 // ═══════════════════════════════════════════════════════════════════════ 1673 'csexp.1.5.2': { 1674 name: 'Csexp', version: '1.5.2', opam: 'csexp', 1675 description: 'Canonical S-expressions (length-prefixed binary format)', 1676 universe: U['csexp.1.5.2'], require: ['csexp'], 1677 sections: [ 1678 { title: 'Encoding', 1679 description: 'Canonical S-expressions use length-prefixed format: "5:hello" instead of "hello".', 1680 steps: [ 1681 { code: 'Csexp.to_string (Csexp.Atom "hello");;', expect: '"5:hello"', 1682 description: 'Encode an atom (5 bytes, colon, data)' }, 1683 { code: 'Csexp.to_string (Csexp.List [Csexp.Atom "a"; Csexp.Atom "bc"]);;', 1684 expect: '"(1:a2:bc)"', description: 'Encode a list' }, 1685 { code: 'Csexp.serialised_length (Csexp.Atom "test");;', expect: '6', 1686 description: '"1:test" would be wrong; "4:test" = 6 bytes' }, 1687 ] }, 1688 { title: 'Decoding', 1689 description: 'parse_string decodes canonical S-expressions.', 1690 steps: [ 1691 { code: 'Csexp.parse_string "5:hello";;', expect: 'Ok', 1692 description: 'Parse a single atom' }, 1693 { code: 'Csexp.parse_string "(1:a2:bc)";;', expect: 'Ok', 1694 description: 'Parse a list' }, 1695 { code: 'Csexp.parse_string_many "1:a1:b";;', expect: 'Ok', 1696 description: 'Parse multiple S-expressions' }, 1697 { code: 'Csexp.parse_string "bad";;', expect: 'Error', 1698 description: 'Invalid input returns Error' }, 1699 ] }, 1700 ], 1701 }, 1702 1703 // ═══════════════════════════════════════════════════════════════════════ 1704 // Base64 1705 // ═══════════════════════════════════════════════════════════════════════ 1706 'base64.3.4.0': { 1707 name: 'Base64', version: '3.4.0', opam: 'base64', 1708 description: 'Base64 encoding and decoding for OCaml', 1709 universe: U['base64.3.4.0'], require: ['base64'], 1710 sections: [ 1711 { title: 'Encoding', 1712 steps: [ 1713 { code: 'Base64.encode_string "Hello, World!";;', expect: 'SGVsbG8sIFdvcmxkIQ==', 1714 description: 'Encode a string to base64' }, 1715 { code: 'Base64.encode_string "";;', expect: '""', 1716 description: 'Empty string encodes to empty' }, 1717 { code: 'Base64.encode_string "a";;', expect: 'YQ==', 1718 description: 'Single character with padding' }, 1719 ] }, 1720 { title: 'Decoding', 1721 steps: [ 1722 { code: 'Base64.decode_exn "SGVsbG8sIFdvcmxkIQ==";;', expect: '"Hello, World!"', 1723 description: 'Decode base64 back to string' }, 1724 { code: 'Base64.decode "YQ==";;', expect: 'Ok "a"', 1725 description: 'Safe decode returns result' }, 1726 { code: 'Base64.decode "!!invalid!!";;', expect: 'Error', 1727 description: 'Invalid base64 returns Error' }, 1728 ] }, 1729 ], 1730 }, 1731 1732 'base64.3.5.2': { 1733 name: 'Base64', version: '3.5.2', opam: 'base64', 1734 description: 'Base64 encoding and decoding for OCaml', 1735 universe: U['base64.3.5.2'], require: ['base64'], 1736 sections: [ 1737 { title: 'Standard Encoding', 1738 steps: [ 1739 { code: 'Base64.encode_string "OCaml";;', expect: 'T0NhbWw=', 1740 description: 'Encode "OCaml" to base64' }, 1741 { code: 'Base64.decode_exn "T0NhbWw=";;', expect: '"OCaml"', 1742 description: 'Decode back to original' }, 1743 ] }, 1744 { title: 'Round-Trip', 1745 steps: [ 1746 { code: 'let test s = Base64.decode_exn (Base64.encode_string s) = s;;', 1747 expect: 'val test', description: 'Define a round-trip test function' }, 1748 { code: 'test "hello world";;', expect: 'true', 1749 description: 'Round-trip preserves data' }, 1750 { code: 'test "";;', expect: 'true', 1751 description: 'Empty string round-trips' }, 1752 { code: 'test "\\x00\\xff";;', expect: 'true', 1753 description: 'Binary data round-trips' }, 1754 ] }, 1755 ], 1756 }, 1757 1758 'bos.0.2.1': { 1759 name: 'Bos', version: '0.2.1', opam: 'bos', 1760 description: 'Basic OS interaction for OCaml', 1761 universe: U['bos.0.2.1'], require: ['bos'], 1762 sections: [ 1763 { title: 'Command Construction', 1764 description: 'Bos.Cmd builds shell commands declaratively with type-safe combinators.', 1765 steps: [ 1766 { code: 'let cmd = Bos.Cmd.(v "echo" % "hello" % "world");;', expect: 'Bos.Cmd.t', 1767 description: 'Build: echo hello world' }, 1768 { code: 'Bos.Cmd.to_string cmd;;', expect: 'echo', 1769 description: 'Convert to shell string' }, 1770 { code: 'Bos.Cmd.to_list cmd;;', expect: '["echo"; "hello"; "world"]', 1771 description: 'Convert to argument list' }, 1772 ] }, 1773 { title: 'Command Combinators', 1774 description: 'Commands support appending, conditional inclusion, and inspection.', 1775 steps: [ 1776 { code: 'let base = Bos.Cmd.(v "gcc" % "-O2");;', expect: 'Bos.Cmd.t', 1777 description: 'Base compiler command' }, 1778 { code: 'let full = Bos.Cmd.(base % "-o" % "main" %% v "main.c");;', expect: 'Bos.Cmd.t', 1779 description: 'Append arguments and a sub-command' }, 1780 { code: 'Bos.Cmd.to_list full;;', expect: '["gcc"', 1781 description: 'Full argument list' }, 1782 { code: 'Bos.Cmd.line_tool full;;', expect: 'Some "gcc"', 1783 description: 'Extract the tool name' }, 1784 { code: 'Bos.Cmd.is_empty Bos.Cmd.empty;;', expect: 'true', 1785 description: 'Empty command check' }, 1786 ] }, 1787 { title: 'Conditional Arguments', 1788 description: 'Bos.Cmd.on conditionally includes arguments.', 1789 steps: [ 1790 { code: 'let debug = true;;', expect: 'true', 1791 description: 'A debug flag' }, 1792 { code: 'Bos.Cmd.(v "gcc" %% on debug (v "-g") % "main.c") |> Bos.Cmd.to_list;;', 1793 expect: '["gcc"; "-g"; "main.c"]', description: 'Debug flag is included when true' }, 1794 { code: 'Bos.Cmd.(v "gcc" %% on false (v "-g") % "main.c") |> Bos.Cmd.to_list;;', 1795 expect: '["gcc"; "main.c"]', description: 'Debug flag is omitted when false' }, 1796 ] }, 1797 ], 1798 }, 1799 1800 // ═══════════════════════════════════════════════════════════════════════ 1801 // Re (regular expressions) 1802 // ═══════════════════════════════════════════════════════════════════════ 1803 're.1.10.4': { 1804 name: 'Re', version: '1.10.4', opam: 're', 1805 description: 'Regular expression library for OCaml', 1806 universe: U['re.1.10.4'], require: ['re'], 1807 sections: [ 1808 { title: 'Compiling and Matching', 1809 description: 'Re works in two steps: build a regex value, then compile it before matching.', 1810 steps: [ 1811 { code: 'let re = Re.Pcre.re "\\\\d+" |> Re.compile;;', expect: 'Re.re', 1812 description: 'Compile a PCRE-style regex for digits' }, 1813 { code: 'Re.execp re "abc123";;', expect: 'true', 1814 description: 'Test if the string matches anywhere' }, 1815 { code: 'Re.execp re "no digits";;', expect: 'false', 1816 description: 'No match returns false' }, 1817 ] }, 1818 { title: 'Extracting Matches', 1819 description: 'Re.exec returns a group object, and Re.Group.get extracts matched substrings.', 1820 steps: [ 1821 { code: 'let g = Re.exec re "abc123def";;', expect: 'Re.Group.t', 1822 description: 'Execute and get the match group' }, 1823 { code: 'Re.Group.get g 0;;', expect: '"123"', 1824 description: 'Group 0 is the whole match' }, 1825 ] }, 1826 { title: 'Finding All Matches', 1827 steps: [ 1828 { code: 'Re.all re "a1b22c333" |> List.map (fun g -> Re.Group.get g 0);;', 1829 expect: '["1"; "22"; "333"]', description: 'Find all digit sequences' }, 1830 { code: 'Re.split (Re.compile (Re.Pcre.re ",")) "a,b,c";;', 1831 expect: '["a"; "b"; "c"]', description: 'Split on comma' }, 1832 ] }, 1833 ], 1834 }, 1835 1836 're.1.11.0': { 1837 name: 'Re', version: '1.11.0', opam: 're', 1838 description: 'Regular expression library for OCaml', 1839 universe: U['re.1.11.0'], require: ['re'], 1840 sections: [ 1841 { title: 'PCRE Syntax', 1842 steps: [ 1843 { code: 'let word = Re.Pcre.re "[a-zA-Z]+" |> Re.compile;;', expect: 'Re.re', 1844 description: 'Compile a word pattern' }, 1845 { code: 'Re.all word "hello world" |> List.map (fun g -> Re.Group.get g 0);;', 1846 expect: '["hello"; "world"]', description: 'Find all words' }, 1847 ] }, 1848 { title: 'Replacement', 1849 steps: [ 1850 { code: 'Re.replace_string (Re.compile (Re.Pcre.re "\\\\d+")) ~by:"N" "abc123def456";;', 1851 expect: '"abcNdefN"', description: 'Replace all digit sequences' }, 1852 ] }, 1853 { title: 'Combinatorial API', 1854 description: 'Re also has a combinator API for building regexes without string syntax.', 1855 steps: [ 1856 { code: 'let re = Re.(seq [bos; rep1 digit; eos]) |> Re.compile;;', expect: 'Re.re', 1857 description: 'Match strings that are all digits' }, 1858 { code: 'Re.execp re "12345";;', expect: 'true', 1859 description: 'All digits matches' }, 1860 { code: 'Re.execp re "123abc";;', expect: 'false', 1861 description: 'Mixed string does not match' }, 1862 ] }, 1863 ], 1864 }, 1865 1866 're.1.12.0': { 1867 name: 'Re', version: '1.12.0', opam: 're', 1868 description: 'Regular expression library for OCaml', 1869 universe: U['re.1.12.0'], require: ['re'], 1870 sections: [ 1871 { title: 'Pattern Matching', 1872 steps: [ 1873 { code: 'let email_re = Re.Pcre.re "[^@]+@[^@]+" |> Re.compile;;', expect: 'Re.re', 1874 description: 'Simple email pattern' }, 1875 { code: 'Re.execp email_re "user@example.com";;', expect: 'true', 1876 description: 'Matches an email-like string' }, 1877 { code: 'Re.execp email_re "not-an-email";;', expect: 'false', 1878 description: 'No @ sign means no match' }, 1879 ] }, 1880 { title: 'Groups', 1881 description: 'Capture groups extract sub-matches.', 1882 steps: [ 1883 { code: 'let kv = Re.Pcre.re "(\\\\w+)=(\\\\w+)" |> Re.compile;;', expect: 'Re.re', 1884 description: 'Key=value pattern with groups' }, 1885 { code: 'let g = Re.exec kv "name=Alice";;', expect: 'Re.Group.t', 1886 description: 'Execute the match' }, 1887 { code: 'Re.Group.get g 1;;', expect: '"name"', 1888 description: 'Group 1: the key' }, 1889 { code: 'Re.Group.get g 2;;', expect: '"Alice"', 1890 description: 'Group 2: the value' }, 1891 ] }, 1892 ], 1893 }, 1894 1895 're.1.13.2': { 1896 name: 'Re', version: '1.13.2', opam: 're', 1897 description: 'Regular expression library for OCaml', 1898 universe: U['re.1.13.2'], require: ['re'], 1899 sections: [ 1900 { title: 'Splitting and Replacing', 1901 steps: [ 1902 { code: 'Re.split (Re.compile (Re.Pcre.re "\\\\s+")) "hello world foo";;', 1903 expect: '["hello"; "world"; "foo"]', description: 'Split on whitespace' }, 1904 { code: 'Re.replace_string (Re.compile (Re.Pcre.re "[aeiou]")) ~by:"*" "hello";;', 1905 expect: '"h*ll*"', description: 'Replace vowels' }, 1906 ] }, 1907 { title: 'Posix Character Classes', 1908 steps: [ 1909 { code: 'let re = Re.(rep1 alpha |> compile);;', expect: 'Re.re', 1910 description: 'Match alphabetic characters' }, 1911 { code: 'Re.execp re "hello";;', expect: 'true', 1912 description: 'All alpha matches' }, 1913 { code: 'Re.all re "abc123def" |> List.map (fun g -> Re.Group.get g 0);;', 1914 expect: '["abc"; "def"]', description: 'Find all alphabetic runs' }, 1915 ] }, 1916 ], 1917 }, 1918 1919 're.1.14.0': { 1920 name: 'Re', version: '1.14.0', opam: 're', 1921 description: 'Regular expression library for OCaml', 1922 universe: U['re.1.14.0'], require: ['re'], 1923 sections: [ 1924 { title: 'Combinatorial API', 1925 steps: [ 1926 { code: 'let hex = Re.(alt [rg \'0\' \'9\'; rg \'a\' \'f\'; rg \'A\' \'F\']) |> Re.rep1 |> Re.compile;;', 1927 expect: 'Re.re', description: 'Match hex strings' }, 1928 { code: 'Re.execp hex "deadBEEF";;', expect: 'true', 1929 description: 'Valid hex matches' }, 1930 { code: 'Re.all hex "ff0099" |> List.map (fun g -> Re.Group.get g 0);;', 1931 expect: '["ff0099"]', description: 'Extract hex values' }, 1932 ] }, 1933 { title: 'Capture Groups', 1934 steps: [ 1935 { code: 'let re = Re.Pcre.re "(\\\\d{4})-(\\\\d{2})" |> Re.compile;;', 1936 expect: 'Re.re', description: 'Date pattern with capture groups' }, 1937 { code: 'let g = Re.exec re "date: 2024-01";;', expect: 'Re.Group.t', 1938 description: 'Execute match' }, 1939 { code: 'Re.Group.get g 1;;', expect: '"2024"', 1940 description: 'First capture group (year)' }, 1941 { code: 'Re.Group.get g 2;;', expect: '"01"', 1942 description: 'Second capture group (month)' }, 1943 ] }, 1944 ], 1945 }, 1946 1947 // ═══════════════════════════════════════════════════════════════════════ 1948 // Angstrom 1949 // ═══════════════════════════════════════════════════════════════════════ 1950 'angstrom.0.15.0': { 1951 name: 'Angstrom', version: '0.15.0', opam: 'angstrom', 1952 description: 'Parser combinators for OCaml', 1953 universe: U['angstrom.0.15.0'], require: ['angstrom'], 1954 sections: [ 1955 { title: 'Basic Parsers', 1956 description: 'Angstrom provides primitive parsers and combinators for building complex parsers.', 1957 steps: [ 1958 { code: 'Angstrom.parse_string ~consume:Prefix (Angstrom.string "hello") "hello world";;', 1959 expect: 'Ok "hello"', description: 'Match a literal string' }, 1960 { code: 'Angstrom.parse_string ~consume:All (Angstrom.string "hello") "hello";;', 1961 expect: 'Ok "hello"', description: 'Consume:All requires full input match' }, 1962 { code: 'Angstrom.parse_string ~consume:All (Angstrom.string "hello") "hello world";;', 1963 expect: 'Error', description: 'Consume:All fails with leftover input' }, 1964 ] }, 1965 { title: 'Character Parsers', 1966 steps: [ 1967 { code: 'let digits = Angstrom.take_while1 (function \'0\'..\'9\' -> true | _ -> false);;', 1968 expect: 'Angstrom.t', description: 'Parser for one or more digits' }, 1969 { code: 'Angstrom.parse_string ~consume:Prefix digits "123abc";;', 1970 expect: 'Ok "123"', description: 'Consume digits, stop at letters' }, 1971 ] }, 1972 { title: 'Combinators', 1973 description: 'Combine parsers with sep_by, many, choice, and operators.', 1974 steps: [ 1975 { code: 'let word = Angstrom.take_while1 (function \'a\'..\'z\' | \'A\'..\'Z\' -> true | _ -> false);;', 1976 expect: 'Angstrom.t', description: 'Parser for words' }, 1977 { code: 'let csv = Angstrom.sep_by (Angstrom.char \',\') word;;', expect: 'Angstrom.t', 1978 description: 'Comma-separated words parser' }, 1979 { code: 'Angstrom.parse_string ~consume:All csv "foo,bar,baz";;', 1980 expect: 'Ok ["foo"; "bar"; "baz"]', description: 'Parse CSV into a list' }, 1981 { code: 'Angstrom.parse_string ~consume:Prefix (Angstrom.many (Angstrom.char \'a\')) "aaab";;', 1982 expect: 'Ok', description: 'many matches zero or more' }, 1983 ] }, 1984 ], 1985 }, 1986 1987 'angstrom.0.16.1': { 1988 name: 'Angstrom', version: '0.16.1', opam: 'angstrom', 1989 description: 'Parser combinators for OCaml', 1990 universe: U['angstrom.0.16.1'], require: ['angstrom'], 1991 sections: [ 1992 { title: 'Parsing Structured Data', 1993 steps: [ 1994 { code: 'let is_digit c = c >= \'0\' && c <= \'9\';;', expect: 'val is_digit', 1995 description: 'Helper: digit predicate' }, 1996 { code: 'let integer = Angstrom.(take_while1 is_digit >>| int_of_string);;', 1997 expect: 'Angstrom.t', description: 'Integer parser using >>| (map)' }, 1998 { code: 'Angstrom.parse_string ~consume:Prefix integer "42rest";;', 1999 expect: 'Ok 42', description: 'Parse and convert to int' }, 2000 ] }, 2001 { title: 'Sequencing and Alternatives', 2002 description: 'Use *> to discard left, <* to discard right, <|> for alternatives.', 2003 steps: [ 2004 { code: 'let bool_p = Angstrom.((string "true" >>| fun _ -> true) <|> (string "false" >>| fun _ -> false));;', 2005 expect: 'Angstrom.t', description: 'Boolean parser with alternatives' }, 2006 { code: 'Angstrom.parse_string ~consume:All bool_p "true";;', 2007 expect: 'Ok true', description: 'Parse "true"' }, 2008 { code: 'Angstrom.parse_string ~consume:All bool_p "false";;', 2009 expect: 'Ok false', description: 'Parse "false"' }, 2010 ] }, 2011 ], 2012 }, 2013 2014 // ═══════════════════════════════════════════════════════════════════════ 2015 // Tyre 2016 // ═══════════════════════════════════════════════════════════════════════ 2017 'tyre.0.5': { 2018 name: 'Tyre', version: '0.5', opam: 'tyre', 2019 description: 'Typed regular expressions for OCaml', 2020 universe: U['tyre.0.5'], require: ['tyre'], 2021 sections: [ 2022 { title: 'Basic Typed Matching', 2023 description: 'Tyre combines regex matching with type extraction.', 2024 steps: [ 2025 { code: 'let re = Tyre.compile Tyre.int;;', expect: 'Tyre.re', 2026 description: 'Compile a typed regex for integers' }, 2027 { code: 'Tyre.exec re "42";;', expect: 'Ok 42', 2028 description: 'Match and extract an int' }, 2029 { code: 'Tyre.exec re "abc";;', expect: 'Error', 2030 description: 'Non-matching input returns Error' }, 2031 ] }, 2032 { title: 'Combining Patterns', 2033 description: 'Use <&> to sequence patterns (returns tuples) and *> or <* to discard parts.', 2034 steps: [ 2035 { code: 'let re = Tyre.compile Tyre.(str "v" *> int);;', expect: 'Tyre.re', 2036 description: 'Match "v" prefix then extract an int' }, 2037 { code: 'Tyre.exec re "v42";;', expect: 'Ok 42', 2038 description: 'Extract version number' }, 2039 { code: 'let dim = Tyre.compile Tyre.(int <&> str "x" *> int);;', expect: 'Tyre.re', 2040 description: 'Match WxH dimension pattern' }, 2041 { code: 'Tyre.exec dim "800x600";;', expect: 'Ok (800, 600)', 2042 description: 'Extract both dimensions as a tuple' }, 2043 ] }, 2044 ], 2045 }, 2046 2047 'tyre.1.0': { 2048 name: 'Tyre', version: '1.0', opam: 'tyre', 2049 description: 'Typed regular expressions for OCaml', 2050 universe: U['tyre.1.0'], require: ['tyre'], 2051 sections: [ 2052 { title: 'Typed Extraction', 2053 steps: [ 2054 { code: 'Tyre.exec (Tyre.compile Tyre.int) "123";;', expect: 'Ok 123', 2055 description: 'Extract an integer' }, 2056 { code: 'Tyre.exec (Tyre.compile Tyre.float) "3.14";;', expect: 'Ok 3.14', 2057 description: 'Extract a float' }, 2058 { code: 'Tyre.exec (Tyre.compile Tyre.bool) "true";;', expect: 'Ok true', 2059 description: 'Extract a boolean' }, 2060 ] }, 2061 { title: 'Optional and Repeated', 2062 steps: [ 2063 { code: 'let re = Tyre.compile Tyre.(opt int);;', expect: 'Tyre.re', 2064 description: 'Optional integer pattern' }, 2065 { code: 'Tyre.exec re "42";;', expect: 'Ok (Some 42)', 2066 description: 'Present value gives Some' }, 2067 { code: 'Tyre.exec re "";;', expect: 'Ok None', 2068 description: 'Empty input gives None' }, 2069 ] }, 2070 { title: 'Bidirectional: Eval', 2071 description: 'Tyre.eval converts values back to strings (unparse).', 2072 steps: [ 2073 { code: 'Tyre.eval Tyre.int 42;;', expect: '"42"', 2074 description: 'Unparse an integer' }, 2075 { code: 'Tyre.eval Tyre.(str "v" *> int) 3;;', expect: '"v3"', 2076 description: 'Unparse with literal prefix' }, 2077 ] }, 2078 ], 2079 }, 2080 2081 // ═══════════════════════════════════════════════════════════════════════ 2082 // Uuseg 2083 // ═══════════════════════════════════════════════════════════════════════ 2084 'uuseg.14.0.0': { 2085 name: 'Uuseg', version: '14.0.0', opam: 'uuseg', 2086 description: 'Unicode text segmentation (Unicode 14.0.0)', 2087 universe: U['uuseg.14.0.0'], require: ['uuseg'], 2088 sections: [ 2089 { title: 'Unicode Version', 2090 steps: [ 2091 { code: 'Uuseg.unicode_version;;', expect: '"14.0.0"', 2092 description: 'Check the Unicode version' }, 2093 ] }, 2094 { title: 'Segmenter Creation', 2095 description: 'Uuseg.create makes a segmenter for grapheme clusters, words, or sentences.', 2096 steps: [ 2097 { code: 'let seg = Uuseg.create `Grapheme_cluster;;', expect: 'Uuseg.t', 2098 description: 'Create a grapheme cluster segmenter' }, 2099 { code: 'let wseg = Uuseg.create `Word;;', expect: 'Uuseg.t', 2100 description: 'Create a word segmenter' }, 2101 ] }, 2102 ], 2103 }, 2104 2105 'uuseg.15.0.0': { 2106 name: 'Uuseg', version: '15.0.0', opam: 'uuseg', 2107 description: 'Unicode text segmentation (Unicode 15.0.0)', 2108 universe: U['uuseg.15.0.0'], require: ['uuseg'], 2109 sections: [ 2110 { title: 'Unicode Version', 2111 steps: [ 2112 { code: 'Uuseg.unicode_version;;', expect: '"15.0.0"', 2113 description: 'Check the Unicode version' }, 2114 ] }, 2115 { title: 'Segmenter Types', 2116 steps: [ 2117 { code: 'let _ = Uuseg.create `Grapheme_cluster;;', expect: 'Uuseg.t', 2118 description: 'Grapheme cluster segmentation' }, 2119 { code: 'let _ = Uuseg.create `Word;;', expect: 'Uuseg.t', 2120 description: 'Word segmentation' }, 2121 { code: 'let _ = Uuseg.create `Sentence;;', expect: 'Uuseg.t', 2122 description: 'Sentence segmentation' }, 2123 { code: 'let _ = Uuseg.create `Line_break;;', expect: 'Uuseg.t', 2124 description: 'Line break opportunity segmentation' }, 2125 ] }, 2126 ], 2127 }, 2128 2129 'uuseg.16.0.0': { 2130 name: 'Uuseg', version: '16.0.0', opam: 'uuseg', 2131 description: 'Unicode text segmentation (Unicode 16.0.0)', 2132 universe: U['uuseg.16.0.0'], require: ['uuseg'], 2133 sections: [ 2134 { title: 'Unicode Version', 2135 steps: [ 2136 { code: 'Uuseg.unicode_version;;', expect: '"16.0.0"', 2137 description: 'Check the Unicode version' }, 2138 ] }, 2139 { title: 'Segmenter API', 2140 description: 'Feed Uchars to a segmenter and it reports segment boundaries.', 2141 steps: [ 2142 { code: 'let seg = Uuseg.create `Grapheme_cluster;;', expect: 'Uuseg.t', 2143 description: 'Create a grapheme cluster segmenter' }, 2144 { code: 'Uuseg.add seg (`Uchar (Uchar.of_int 0x0041));;', expect: '', 2145 description: "Add 'A' to the segmenter" }, 2146 { code: 'Uuseg.add seg `End;;', expect: '', 2147 description: 'Signal end of input' }, 2148 ] }, 2149 ], 2150 }, 2151 2152 'uuseg.17.0.0': { 2153 name: 'Uuseg', version: '17.0.0', opam: 'uuseg', 2154 description: 'Unicode text segmentation (Unicode 17.0.0)', 2155 universe: U['uuseg.17.0.0'], require: ['uuseg'], 2156 sections: [ 2157 { title: 'Unicode Version', 2158 steps: [ 2159 { code: 'Uuseg.unicode_version;;', expect: '"17.0.0"', 2160 description: 'Check the Unicode version' }, 2161 ] }, 2162 { title: 'Segmenter Types', 2163 steps: [ 2164 { code: 'let _ = Uuseg.create `Grapheme_cluster;;', expect: 'Uuseg.t', 2165 description: 'Grapheme cluster boundaries' }, 2166 { code: 'let _ = Uuseg.create `Word;;', expect: 'Uuseg.t', 2167 description: 'Word boundaries' }, 2168 { code: 'let _ = Uuseg.create `Sentence;;', expect: 'Uuseg.t', 2169 description: 'Sentence boundaries' }, 2170 ] }, 2171 ], 2172 }, 2173 2174 // ═══════════════════════════════════════════════════════════════════════ 2175 // Containers 2176 // ═══════════════════════════════════════════════════════════════════════ 2177 'containers.3.17': { 2178 name: 'Containers', version: '3.17', opam: 'containers', 2179 description: 'A modular extension of the OCaml standard library', 2180 universe: U['containers.3.17'], require: ['containers'], 2181 sections: [ 2182 { title: 'CCList Advanced', 2183 steps: [ 2184 { code: 'CCList.product (fun a b -> (a, b)) [1; 2] ["a"; "b"];;', 2185 expect: '[(1, "a")', description: 'Cartesian product' }, 2186 { code: 'CCList.pure 42;;', expect: '[42]', 2187 description: 'Wrap a value in a singleton list' }, 2188 ] }, 2189 { title: 'CCString', 2190 steps: [ 2191 { code: 'CCString.take 5 "hello world";;', expect: '"hello"', 2192 description: 'Take first 5 characters' }, 2193 { code: 'CCString.drop 6 "hello world";;', expect: '"world"', 2194 description: 'Drop first 6 characters' }, 2195 { code: 'CCString.chop_prefix ~pre:"http://" "http://example.com";;', 2196 expect: 'Some "example.com"', description: 'Remove prefix if present' }, 2197 { code: 'CCString.chop_suffix ~suf:".ml" "main.ml";;', 2198 expect: 'Some "main"', description: 'Remove suffix if present' }, 2199 ] }, 2200 ], 2201 }, 2202 2203 // ═══════════════════════════════════════════════════════════════════════ 2204 // Iter 2205 // ═══════════════════════════════════════════════════════════════════════ 2206 'iter.1.7': { 2207 name: 'Iter', version: '1.7', opam: 'iter', 2208 description: 'Simple, efficient iterators for OCaml', 2209 universe: U['iter.1.7'], require: ['iter'], 2210 sections: [ 2211 { title: 'Creating Iterators', 2212 description: 'Iter.t is (\'a -> unit) -> unit — a continuation-based iterator.', 2213 steps: [ 2214 { code: 'Iter.of_list [1; 2; 3] |> Iter.to_list;;', expect: '[1; 2; 3]', 2215 description: 'Round-trip through Iter' }, 2216 { code: 'Iter.(1 -- 5) |> Iter.to_list;;', expect: '[1; 2; 3; 4; 5]', 2217 description: 'Integer range (inclusive)' }, 2218 { code: 'Iter.init (fun i -> i * i) |> Iter.take 5 |> Iter.to_list;;', 2219 expect: '[0; 1; 4; 9; 16]', description: 'Infinite sequence, take first 5' }, 2220 ] }, 2221 { title: 'Transformations', 2222 steps: [ 2223 { code: 'Iter.(1 -- 10) |> Iter.filter (fun x -> x mod 2 = 0) |> Iter.to_list;;', 2224 expect: '[2; 4; 6; 8; 10]', description: 'Filter even numbers' }, 2225 { code: 'Iter.(1 -- 5) |> Iter.map (fun x -> x * 2) |> Iter.to_list;;', 2226 expect: '[2; 4; 6; 8; 10]', description: 'Map doubling' }, 2227 { code: 'Iter.(1 -- 5) |> Iter.fold (+) 0;;', expect: '15', 2228 description: 'Fold to compute sum' }, 2229 ] }, 2230 ], 2231 }, 2232 2233 'iter.1.8': { 2234 name: 'Iter', version: '1.8', opam: 'iter', 2235 description: 'Simple, efficient iterators for OCaml', 2236 universe: U['iter.1.8'], require: ['iter'], 2237 sections: [ 2238 { title: 'Iterator Basics', 2239 steps: [ 2240 { code: 'Iter.empty |> Iter.to_list;;', expect: '[]', 2241 description: 'Empty iterator' }, 2242 { code: 'Iter.singleton 42 |> Iter.to_list;;', expect: '[42]', 2243 description: 'Single-element iterator' }, 2244 { code: 'Iter.repeat 3 |> Iter.take 4 |> Iter.to_list;;', expect: '[3; 3; 3; 3]', 2245 description: 'Infinite repetition, take 4' }, 2246 ] }, 2247 { title: 'Flat Map and Product', 2248 steps: [ 2249 { code: 'Iter.(1 -- 3) |> Iter.flat_map (fun x -> Iter.of_list [x; x*10]) |> Iter.to_list;;', 2250 expect: '[1; 10; 2; 20; 3; 30]', description: 'Flat map' }, 2251 { code: 'Iter.product (Iter.of_list [1;2]) (Iter.of_list ["a";"b"]) |> Iter.to_list;;', 2252 expect: '[(1, "a")', description: 'Cartesian product' }, 2253 ] }, 2254 ], 2255 }, 2256 2257 'iter.1.9': { 2258 name: 'Iter', version: '1.9', opam: 'iter', 2259 description: 'Simple, efficient iterators for OCaml', 2260 universe: U['iter.1.9'], require: ['iter'], 2261 sections: [ 2262 { title: 'Aggregation', 2263 steps: [ 2264 { code: 'Iter.(1 -- 100) |> Iter.fold (+) 0;;', expect: '5050', 2265 description: 'Sum 1 to 100' }, 2266 { code: 'Iter.(1 -- 10) |> Iter.length;;', expect: '10', 2267 description: 'Count elements' }, 2268 { code: 'Iter.of_list ["hello"; "world"] |> Iter.for_all (fun s -> String.length s > 3);;', 2269 expect: 'true', description: 'Check a predicate for all elements' }, 2270 { code: 'Iter.of_list [1; 2; 3] |> Iter.exists (fun x -> x > 2);;', 2271 expect: 'true', description: 'Check if any element matches' }, 2272 ] }, 2273 { title: 'Conversion', 2274 steps: [ 2275 { code: 'Iter.of_list [1; 2; 3] |> Iter.to_rev_list;;', expect: '[3; 2; 1]', 2276 description: 'Convert to reversed list' }, 2277 { code: 'Iter.of_list [("a",1); ("b",2)] |> Iter.to_hashtbl;;', expect: 'Hashtbl', 2278 description: 'Convert to hashtable' }, 2279 ] }, 2280 ], 2281 }, 2282 2283 // ═══════════════════════════════════════════════════════════════════════ 2284 // OCamlgraph 2285 // ═══════════════════════════════════════════════════════════════════════ 2286 'ocamlgraph.2.0.0': { 2287 name: 'OCamlgraph', version: '2.0.0', opam: 'ocamlgraph', 2288 description: 'Graph library for OCaml', 2289 universe: U['ocamlgraph.2.0.0'], require: ['ocamlgraph'], 2290 sections: [ 2291 { title: 'Building Graphs', 2292 description: 'Graph.Pack.Digraph provides an easy-to-use imperative directed graph. Vertices must be reused (not re-created).', 2293 steps: [ 2294 { code: 'let module G = Graph.Pack.Digraph in let g = G.create () in let v1 = G.V.create 1 in let v2 = G.V.create 2 in let v3 = G.V.create 3 in G.add_edge g v1 v2; G.add_edge g v2 v3; G.nb_vertex g;;', 2295 expect: '3', description: 'Create a graph with 3 vertices' }, 2296 { code: 'let module G = Graph.Pack.Digraph in let g = G.create () in let v1 = G.V.create 1 in let v2 = G.V.create 2 in let v3 = G.V.create 3 in G.add_edge g v1 v2; G.add_edge g v1 v3; G.nb_edges g;;', 2297 expect: '2', description: '2 edges from vertex 1' }, 2298 ] }, 2299 ], 2300 }, 2301 2302 'ocamlgraph.2.1.0': { 2303 name: 'OCamlgraph', version: '2.1.0', opam: 'ocamlgraph', 2304 description: 'Graph library for OCaml', 2305 universe: U['ocamlgraph.2.1.0'], require: ['ocamlgraph'], 2306 sections: [ 2307 { title: 'Imperative Graphs', 2308 steps: [ 2309 { code: 'let module G = Graph.Pack.Digraph in let g = G.create () in let v1 = G.V.create 10 in let v2 = G.V.create 20 in G.add_edge g v1 v2; G.mem_edge g v1 v2;;', 2310 expect: 'true', description: 'Check edge existence' }, 2311 { code: 'let module G = Graph.Pack.Digraph in let g = G.create () in G.add_edge g (G.V.create 1) (G.V.create 2); G.add_edge g (G.V.create 2) (G.V.create 3); G.add_edge g (G.V.create 3) (G.V.create 1); G.nb_edges g;;', 2312 expect: '3', description: 'A cycle with 3 edges' }, 2313 ] }, 2314 ], 2315 }, 2316 2317 'ocamlgraph.2.2.0': { 2318 name: 'OCamlgraph', version: '2.2.0', opam: 'ocamlgraph', 2319 description: 'Graph library for OCaml', 2320 universe: U['ocamlgraph.2.2.0'], require: ['ocamlgraph'], 2321 sections: [ 2322 { title: 'Graph Operations', 2323 steps: [ 2324 { code: 'let module G = Graph.Pack.Digraph in let g = G.create () in let vs = Array.init 6 G.V.create in for i = 0 to 4 do G.add_edge g vs.(i) vs.(i+1) done; G.nb_vertex g;;', 2325 expect: '6', description: 'A chain of 6 vertices' }, 2326 { code: 'let module G = Graph.Pack.Digraph in let g = G.create () in let vs = Array.init 6 G.V.create in for i = 0 to 4 do G.add_edge g vs.(i) vs.(i+1) done; G.nb_edges g;;', 2327 expect: '5', description: '5 edges in the chain' }, 2328 { code: 'let module G = Graph.Pack.Digraph in let g = G.create () in let v0 = G.V.create 0 in let v1 = G.V.create 1 in let v2 = G.V.create 2 in G.add_edge g v0 v1; G.add_edge g v0 v2; G.out_degree g v0;;', 2329 expect: '2', description: 'Out-degree of vertex 0' }, 2330 ] }, 2331 ], 2332 }, 2333 2334 // ═══════════════════════════════════════════════════════════════════════ 2335 // Digestif 2336 // ═══════════════════════════════════════════════════════════════════════ 2337 'digestif.1.1.2': { 2338 name: 'Digestif', version: '1.1.2', opam: 'digestif', 2339 description: 'Cryptographic hash functions for OCaml', 2340 universe: U['digestif.1.1.2'], require: ['digestif'], 2341 sections: [ 2342 { title: 'SHA-256', 2343 description: 'Digestif.SHA256 provides SHA-256 hashing with hex encoding.', 2344 steps: [ 2345 { code: 'Digestif.SHA256.digest_string "hello" |> Digestif.SHA256.to_hex;;', 2346 expect: '2cf24dba', description: 'SHA-256 of "hello"' }, 2347 { code: 'Digestif.SHA256.digest_string "" |> Digestif.SHA256.to_hex;;', 2348 expect: 'e3b0c442', description: 'SHA-256 of empty string' }, 2349 { code: 'let h1 = Digestif.SHA256.digest_string "test" in let h2 = Digestif.SHA256.digest_string "test" in Digestif.SHA256.equal h1 h2;;', 2350 expect: 'true', description: 'Same input produces same hash (constant-time equal)' }, 2351 ] }, 2352 { title: 'MD5', 2353 steps: [ 2354 { code: 'Digestif.MD5.digest_string "hello" |> Digestif.MD5.to_hex;;', 2355 expect: '5d41402a', description: 'MD5 of "hello"' }, 2356 ] }, 2357 ], 2358 }, 2359 2360 'digestif.1.3.0': { 2361 name: 'Digestif', version: '1.3.0', opam: 'digestif', 2362 description: 'Cryptographic hash functions for OCaml', 2363 universe: U['digestif.1.3.0'], require: ['digestif'], 2364 sections: [ 2365 { title: 'Multiple Algorithms', 2366 steps: [ 2367 { code: 'Digestif.SHA256.digest_string "OCaml" |> Digestif.SHA256.to_hex;;', 2368 expect: 'string', description: 'SHA-256 hash' }, 2369 { code: 'Digestif.SHA512.digest_string "OCaml" |> Digestif.SHA512.to_hex;;', 2370 expect: 'string', description: 'SHA-512 hash' }, 2371 { code: 'Digestif.SHA1.digest_string "OCaml" |> Digestif.SHA1.to_hex;;', 2372 expect: 'string', description: 'SHA-1 hash' }, 2373 ] }, 2374 { title: 'HMAC', 2375 description: 'HMAC provides keyed hashing for authentication.', 2376 steps: [ 2377 { code: 'Digestif.SHA256.hmac_string ~key:"secret" "message" |> Digestif.SHA256.to_hex;;', 2378 expect: 'string', description: 'HMAC-SHA256 with a key' }, 2379 { code: 'let h1 = Digestif.SHA256.hmac_string ~key:"k" "m" in let h2 = Digestif.SHA256.hmac_string ~key:"k" "m" in Digestif.SHA256.equal h1 h2;;', 2380 expect: 'true', description: 'Same key+message = same HMAC' }, 2381 ] }, 2382 ], 2383 }, 2384 2385 // ═══════════════════════════════════════════════════════════════════════ 2386 // Hex 2387 // ═══════════════════════════════════════════════════════════════════════ 2388 'hex.1.4.0': { 2389 name: 'Hex', version: '1.4.0', opam: 'hex', 2390 description: 'Hex encoding and decoding for OCaml', 2391 universe: U['hex.1.4.0'], require: ['hex'], 2392 sections: [ 2393 { title: 'Encoding and Decoding', 2394 steps: [ 2395 { code: 'Hex.of_string "Hello";;', expect: '`Hex', 2396 description: 'Encode string to hex' }, 2397 { code: 'Hex.to_string (`Hex "48656c6c6f");;', expect: '"Hello"', 2398 description: 'Decode hex to string' }, 2399 { code: 'Hex.hexdump_s (Hex.of_string "Hello, World!");;', expect: '4865', 2400 description: 'Hexdump for debugging' }, 2401 ] }, 2402 ], 2403 }, 2404 2405 'hex.1.5.0': { 2406 name: 'Hex', version: '1.5.0', opam: 'hex', 2407 description: 'Hex encoding and decoding for OCaml', 2408 universe: U['hex.1.5.0'], require: ['hex'], 2409 sections: [ 2410 { title: 'Hex Encoding', 2411 steps: [ 2412 { code: 'Hex.of_string "\\x00\\xff";;', expect: '`Hex "00ff"', 2413 description: 'Binary data to hex' }, 2414 { code: 'Hex.to_string (`Hex "00ff");;', expect: 'string', 2415 description: 'Hex back to binary' }, 2416 { code: 'Hex.show (Hex.of_string "AB");;', expect: '"4142"', 2417 description: 'Show hex representation' }, 2418 ] }, 2419 ], 2420 }, 2421 2422 // ═══════════════════════════════════════════════════════════════════════ 2423 // Eqaf 2424 // ═══════════════════════════════════════════════════════════════════════ 2425 'eqaf.0.9': { 2426 name: 'Eqaf', version: '0.9', opam: 'eqaf', 2427 description: 'Constant-time string comparison for OCaml', 2428 universe: U['eqaf.0.9'], require: ['eqaf'], 2429 sections: [ 2430 { title: 'Constant-Time Comparison', 2431 description: 'Eqaf.equal compares strings in constant time, preventing timing attacks.', 2432 steps: [ 2433 { code: 'Eqaf.equal "secret" "secret";;', expect: 'true', 2434 description: 'Equal strings' }, 2435 { code: 'Eqaf.equal "secret" "wrong!";;', expect: 'false', 2436 description: 'Different strings' }, 2437 { code: 'Eqaf.equal "" "";;', expect: 'true', 2438 description: 'Empty strings are equal' }, 2439 ] }, 2440 ], 2441 }, 2442 2443 'eqaf.0.10': { 2444 name: 'Eqaf', version: '0.10', opam: 'eqaf', 2445 description: 'Constant-time string comparison for OCaml', 2446 universe: U['eqaf.0.10'], require: ['eqaf'], 2447 sections: [ 2448 { title: 'Constant-Time Operations', 2449 steps: [ 2450 { code: 'Eqaf.equal "abc" "abc";;', expect: 'true', 2451 description: 'Same strings (constant time)' }, 2452 { code: 'Eqaf.equal "abc" "xyz";;', expect: 'false', 2453 description: 'Different strings (same timing as equal case)' }, 2454 { code: 'Eqaf.compare_be "a" "b";;', expect: '-1', 2455 description: 'Constant-time big-endian comparison' }, 2456 ] }, 2457 ], 2458 }, 2459 2460 // ═══════════════════════════════════════════════════════════════════════ 2461 // Uri 2462 // ═══════════════════════════════════════════════════════════════════════ 2463 'uri.4.2.0': { 2464 name: 'Uri', version: '4.2.0', opam: 'uri', 2465 description: 'URI parsing and manipulation for OCaml', 2466 universe: U['uri.4.2.0'], require: ['uri'], 2467 sections: [ 2468 { title: 'Parsing URIs', 2469 steps: [ 2470 { code: 'let u = Uri.of_string "https://example.com:8080/path?q=1#frag";;', expect: 'Uri.t', 2471 description: 'Parse a full URI' }, 2472 { code: 'Uri.scheme u;;', expect: 'Some "https"', 2473 description: 'Extract the scheme' }, 2474 { code: 'Uri.host u;;', expect: 'Some "example.com"', 2475 description: 'Extract the host' }, 2476 { code: 'Uri.port u;;', expect: 'Some 8080', 2477 description: 'Extract the port' }, 2478 { code: 'Uri.path u;;', expect: '"/path"', 2479 description: 'Extract the path' }, 2480 { code: 'Uri.fragment u;;', expect: 'Some "frag"', 2481 description: 'Extract the fragment' }, 2482 ] }, 2483 { title: 'Query Parameters', 2484 steps: [ 2485 { code: 'let u = Uri.of_string "http://example.com?a=1&b=2";;', expect: 'Uri.t', 2486 description: 'URI with query params' }, 2487 { code: 'Uri.get_query_param u "a";;', expect: 'Some "1"', 2488 description: 'Get a single query parameter' }, 2489 { code: 'Uri.query u;;', expect: '[("a"', description: 'Get all query parameters' }, 2490 ] }, 2491 { title: 'Building URIs', 2492 steps: [ 2493 { code: 'Uri.make ~scheme:"https" ~host:"example.com" ~path:"/api" () |> Uri.to_string;;', 2494 expect: 'https://example.com/api', description: 'Build a URI from components' }, 2495 { code: 'Uri.with_query\' (Uri.of_string "http://x.com") [("key", "val")] |> Uri.to_string;;', 2496 expect: 'key=val', description: 'Add query parameters' }, 2497 ] }, 2498 ], 2499 }, 2500 2501 'uri.4.4.0': { 2502 name: 'Uri', version: '4.4.0', opam: 'uri', 2503 description: 'URI parsing and manipulation for OCaml', 2504 universe: U['uri.4.4.0'], require: ['uri'], 2505 sections: [ 2506 { title: 'URI Components', 2507 steps: [ 2508 { code: 'let u = Uri.of_string "ftp://user@host/file.txt";;', expect: 'Uri.t', 2509 description: 'Parse an FTP URI' }, 2510 { code: 'Uri.scheme u;;', expect: 'Some "ftp"', 2511 description: 'FTP scheme' }, 2512 { code: 'Uri.userinfo u;;', expect: 'Some "user"', 2513 description: 'Extract userinfo' }, 2514 { code: 'Uri.host u;;', expect: 'Some "host"', 2515 description: 'Extract host' }, 2516 { code: 'Uri.path u;;', expect: '"/file.txt"', 2517 description: 'Extract path' }, 2518 ] }, 2519 { title: 'URI Manipulation', 2520 steps: [ 2521 { code: 'let u = Uri.of_string "http://example.com/old" in Uri.with_path u "/new" |> Uri.to_string;;', 2522 expect: 'http://example.com/new', description: 'Replace the path' }, 2523 { code: 'Uri.resolve "http" (Uri.of_string "http://example.com/a/b") (Uri.of_string "../c") |> Uri.to_string;;', 2524 expect: 'example.com', description: 'Resolve a relative reference' }, 2525 ] }, 2526 ], 2527 }, 2528 2529 // ═══════════════════════════════════════════════════════════════════════ 2530 // Ipaddr 2531 // ═══════════════════════════════════════════════════════════════════════ 2532 'ipaddr.5.6.0': { 2533 name: 'Ipaddr', version: '5.6.0', opam: 'ipaddr', 2534 description: 'IP address parsing and manipulation for OCaml', 2535 universe: U['ipaddr.5.6.0'], require: ['ipaddr'], 2536 sections: [ 2537 { title: 'IPv4 Addresses', 2538 steps: [ 2539 { code: 'Ipaddr.V4.of_string_exn "192.168.1.1";;', expect: 'Ipaddr.V4.t', 2540 description: 'Parse an IPv4 address' }, 2541 { code: 'Ipaddr.V4.to_string (Ipaddr.V4.of_string_exn "10.0.0.1");;', 2542 expect: '"10.0.0.1"', description: 'Convert back to string' }, 2543 { code: 'Ipaddr.V4.localhost |> Ipaddr.V4.to_string;;', expect: '"127.0.0.1"', 2544 description: 'Localhost constant' }, 2545 ] }, 2546 { title: 'IPv6 Addresses', 2547 steps: [ 2548 { code: 'Ipaddr.V6.of_string_exn "::1" |> Ipaddr.V6.to_string;;', expect: '"::1"', 2549 description: 'IPv6 loopback' }, 2550 { code: 'Ipaddr.V6.localhost |> Ipaddr.V6.to_string;;', expect: '"::1"', 2551 description: 'IPv6 localhost constant' }, 2552 ] }, 2553 { title: 'Generic IP', 2554 steps: [ 2555 { code: 'Ipaddr.of_string_exn "192.168.1.1" |> Ipaddr.to_string;;', 2556 expect: '"192.168.1.1"', description: 'Parse any IP address' }, 2557 { code: 'Ipaddr.of_string_exn "::1" |> Ipaddr.to_string;;', 2558 expect: '"::1"', description: 'Parse IPv6 through generic interface' }, 2559 ] }, 2560 ], 2561 }, 2562 2563 'ipaddr.5.6.1': { 2564 name: 'Ipaddr', version: '5.6.1', opam: 'ipaddr', 2565 description: 'IP address parsing and manipulation for OCaml', 2566 universe: U['ipaddr.5.6.1'], require: ['ipaddr'], 2567 sections: [ 2568 { title: 'Address Operations', 2569 steps: [ 2570 { code: 'Ipaddr.V4.of_string "invalid";;', expect: 'Error', 2571 description: 'Invalid address returns Error' }, 2572 { code: 'Ipaddr.V4.of_string "10.0.0.1";;', expect: 'Ok', 2573 description: 'Valid address returns Ok' }, 2574 { code: 'Ipaddr.V4.(compare localhost (of_string_exn "127.0.0.1"));;', expect: '0', 2575 description: 'Localhost equals 127.0.0.1' }, 2576 ] }, 2577 { title: 'CIDR Prefixes', 2578 steps: [ 2579 { code: 'let prefix = Ipaddr.V4.Prefix.of_string_exn "192.168.0.0/24";;', 2580 expect: 'Ipaddr.V4.Prefix.t', description: 'Parse a CIDR prefix' }, 2581 { code: 'Ipaddr.V4.Prefix.mem (Ipaddr.V4.of_string_exn "192.168.0.42") prefix;;', 2582 expect: 'true', description: 'Address is in the prefix' }, 2583 { code: 'Ipaddr.V4.Prefix.mem (Ipaddr.V4.of_string_exn "192.168.1.1") prefix;;', 2584 expect: 'false', description: 'Address is not in the prefix' }, 2585 ] }, 2586 ], 2587 }, 2588 2589 // ═══════════════════════════════════════════════════════════════════════ 2590 // Domain_name 2591 // ═══════════════════════════════════════════════════════════════════════ 2592 'domain-name.0.4.1': { 2593 name: 'Domain_name', version: '0.4.1', opam: 'domain-name', 2594 description: 'Domain name parsing and validation for OCaml', 2595 universe: U['domain-name.0.4.1'], require: ['domain-name'], 2596 sections: [ 2597 { title: 'Parsing Domain Names', 2598 steps: [ 2599 { code: 'Domain_name.of_string_exn "example.com";;', expect: 'Domain_name.t', 2600 description: 'Parse a domain name' }, 2601 { code: 'Domain_name.to_string (Domain_name.of_string_exn "www.example.com");;', 2602 expect: '"www.example.com"', description: 'Convert back to string' }, 2603 { code: 'Domain_name.of_string "invalid..domain";;', expect: 'Error', 2604 description: 'Double dots are invalid' }, 2605 ] }, 2606 { title: 'Domain Name Operations', 2607 steps: [ 2608 { code: 'Domain_name.of_string_exn "sub.example.com" |> Domain_name.count_labels;;', 2609 expect: '3', description: 'Count labels (sub, example, com)' }, 2610 { code: 'Domain_name.equal (Domain_name.of_string_exn "A.COM") (Domain_name.of_string_exn "a.com");;', 2611 expect: 'true', description: 'Domain names are case-insensitive' }, 2612 ] }, 2613 ], 2614 }, 2615 2616 'domain-name.0.5.0': { 2617 name: 'Domain_name', version: '0.5.0', opam: 'domain-name', 2618 description: 'Domain name parsing and validation for OCaml', 2619 universe: U['domain-name.0.5.0'], require: ['domain-name'], 2620 sections: [ 2621 { title: 'Domain Names', 2622 steps: [ 2623 { code: 'Domain_name.of_string_exn "mail.example.org";;', expect: 'Domain_name.t', 2624 description: 'Parse a domain name' }, 2625 { code: 'Domain_name.of_string_exn "example.com" |> Domain_name.count_labels;;', 2626 expect: '2', description: 'Two labels' }, 2627 { code: 'Domain_name.is_subdomain ~subdomain:(Domain_name.of_string_exn "sub.example.com") ~domain:(Domain_name.of_string_exn "example.com");;', 2628 expect: 'true', description: 'Check subdomain relationship' }, 2629 ] }, 2630 { title: 'Host Names', 2631 description: 'Domain_name.host_exn validates a domain name as a valid hostname.', 2632 steps: [ 2633 { code: 'Domain_name.host_exn (Domain_name.of_string_exn "example.com");;', 2634 expect: 'Domain_name.t', description: 'Valid hostname' }, 2635 { code: 'Domain_name.to_string (Domain_name.of_string_exn "example.com");;', 2636 expect: '"example.com"', description: 'Convert back to string' }, 2637 ] }, 2638 ], 2639 }, 2640 2641 // ═══════════════════════════════════════════════════════════════════════ 2642 // Zarith 2643 // ═══════════════════════════════════════════════════════════════════════ 2644 'zarith.1.13': { 2645 name: 'Zarith', version: '1.13', opam: 'zarith', 2646 description: 'Arbitrary-precision integers and rationals for OCaml', 2647 universe: U['zarith.1.13'], require: ['zarith'], 2648 sections: [ 2649 { title: 'Big Integers', 2650 description: 'Z.t represents arbitrary-precision integers.', 2651 steps: [ 2652 { code: 'Z.of_int 42;;', expect: '42', description: 'Create from int' }, 2653 { code: 'Z.of_string "999999999999999999999";;', expect: '999999999999999999999', 2654 description: 'Create from string (exceeds int range)' }, 2655 { code: 'Z.add (Z.of_int 1) (Z.of_string "999999999999999999999");;', 2656 expect: '1000000000000000000000', description: 'Arbitrary-precision addition' }, 2657 ] }, 2658 { title: 'Arithmetic', 2659 steps: [ 2660 { code: 'Z.mul (Z.of_int 1000000) (Z.of_int 1000000);;', expect: '1000000000000', 2661 description: 'Multiplication' }, 2662 { code: 'Z.pow (Z.of_int 2) 100 |> Z.to_string;;', expect: '1267650600228229401496703205376', 2663 description: '2^100 as a string' }, 2664 { code: 'Z.rem (Z.of_int 17) (Z.of_int 5);;', expect: '2', 2665 description: 'Remainder' }, 2666 { code: 'Z.gcd (Z.of_int 12) (Z.of_int 18);;', expect: '6', 2667 description: 'Greatest common divisor' }, 2668 ] }, 2669 { title: 'Comparison', 2670 steps: [ 2671 { code: 'Z.compare (Z.of_int 10) (Z.of_int 20);;', expect: '-1', 2672 description: '10 < 20' }, 2673 { code: 'Z.equal Z.zero Z.zero;;', expect: 'true', 2674 description: 'Zero equals zero' }, 2675 { code: 'Z.sign (Z.of_int (-5));;', expect: '-1', 2676 description: 'Sign of negative number' }, 2677 ] }, 2678 ], 2679 }, 2680 2681 'zarith.1.14': { 2682 name: 'Zarith', version: '1.14', opam: 'zarith', 2683 description: 'Arbitrary-precision integers and rationals for OCaml', 2684 universe: U['zarith.1.14'], require: ['zarith'], 2685 sections: [ 2686 { title: 'Big Integer Arithmetic', 2687 steps: [ 2688 { code: 'Z.(of_int 2 ** 256) |> Z.to_string |> String.length;;', 2689 expect: '78', description: '2^256 has 78 digits' }, 2690 { code: 'Z.probab_prime (Z.of_int 97) 25;;', expect: '2', 2691 description: '97 is prime (2 = definitely prime)' }, 2692 { code: 'Z.probab_prime (Z.of_int 100) 25;;', expect: '0', 2693 description: '100 is composite (0 = definitely not prime)' }, 2694 ] }, 2695 { title: 'Rationals (Q module)', 2696 description: 'Q.t represents exact rational numbers.', 2697 steps: [ 2698 { code: 'Q.of_ints 1 3;;', expect: '1/3', 2699 description: 'Create the fraction 1/3' }, 2700 { code: 'Q.add (Q.of_ints 1 3) (Q.of_ints 1 6);;', expect: '1/2', 2701 description: '1/3 + 1/6 = 1/2 (auto-simplified)' }, 2702 { code: 'Q.mul (Q.of_ints 2 3) (Q.of_ints 3 4);;', expect: '1/2', 2703 description: '2/3 * 3/4 = 1/2' }, 2704 { code: 'Q.to_float (Q.of_ints 1 3);;', expect: '0.333333', 2705 description: 'Convert to float (approximate)' }, 2706 ] }, 2707 ], 2708 }, 2709 2710 // ═══════════════════════════════════════════════════════════════════════ 2711 // QCheck 2712 // ═══════════════════════════════════════════════════════════════════════ 2713 'qcheck-core.0.25': { 2714 name: 'QCheck', version: '0.25', opam: 'qcheck-core', 2715 description: 'Property-based testing for OCaml', 2716 universe: U['qcheck-core.0.25'], require: ['qcheck-core'], 2717 sections: [ 2718 { title: 'Generators', 2719 description: 'QCheck2.Gen provides random value generators with integrated shrinking.', 2720 steps: [ 2721 { code: 'QCheck2.Gen.generate1 QCheck2.Gen.int;;', expect: 'int', 2722 description: 'Generate a random integer' }, 2723 { code: 'QCheck2.Gen.generate1 (QCheck2.Gen.return 42);;', expect: '42', 2724 description: 'Constant generator always returns 42' }, 2725 { code: 'QCheck2.Gen.generate1 (QCheck2.Gen.list QCheck2.Gen.small_int) |> List.length >= 0;;', 2726 expect: 'true', description: 'Generate a random list of small ints' }, 2727 ] }, 2728 { title: 'Property Tests', 2729 description: 'QCheck2.Test.make creates a test, check_exn runs it.', 2730 steps: [ 2731 { code: 'let t = QCheck2.Test.make ~name:"commutative" QCheck2.Gen.(pair int int) (fun (a, b) -> a + b = b + a);;', 2732 expect: 'QCheck2.Test.t', description: 'Addition is commutative' }, 2733 { code: 'QCheck2.Test.check_exn t;;', expect: 'unit', 2734 description: 'Test passes (no exception)' }, 2735 { code: 'let t2 = QCheck2.Test.make ~name:"rev rev" QCheck2.Gen.(list small_int) (fun l -> List.rev (List.rev l) = l);;', 2736 expect: 'QCheck2.Test.t', description: 'Double reverse is identity' }, 2737 { code: 'QCheck2.Test.check_exn t2;;', expect: 'unit', 2738 description: 'Test passes' }, 2739 ] }, 2740 ], 2741 }, 2742 2743 'qcheck-core.0.27': { 2744 name: 'QCheck', version: '0.27', opam: 'qcheck-core', 2745 description: 'Property-based testing for OCaml', 2746 universe: U['qcheck-core.0.27'], require: ['qcheck-core'], 2747 sections: [ 2748 { title: 'Generators', 2749 steps: [ 2750 { code: 'QCheck2.Gen.generate1 (QCheck2.Gen.oneof [QCheck2.Gen.return 1; QCheck2.Gen.return 2]);;', 2751 expect: 'int', description: 'Choose between generators randomly' }, 2752 { code: 'QCheck2.Gen.generate1 (QCheck2.Gen.map (fun x -> x * 2) QCheck2.Gen.small_int);;', 2753 expect: 'int', description: 'Map over a generator' }, 2754 ] }, 2755 { title: 'Testing Properties', 2756 steps: [ 2757 { code: 'let t = QCheck2.Test.make ~name:"sort idempotent" QCheck2.Gen.(list small_int) (fun l -> let s = List.sort compare l in List.sort compare s = s);;', 2758 expect: 'QCheck2.Test.t', description: 'Sorting is idempotent' }, 2759 { code: 'QCheck2.Test.check_exn t;;', expect: 'unit', 2760 description: 'Property holds' }, 2761 { code: 'let t = QCheck2.Test.make ~count:1000 ~name:"length" QCheck2.Gen.(list small_int) (fun l -> List.length (List.rev l) = List.length l);;', 2762 expect: 'QCheck2.Test.t', description: 'Rev preserves length (1000 tests)' }, 2763 { code: 'QCheck2.Test.check_exn t;;', expect: 'unit', 2764 description: 'Passes all 1000 tests' }, 2765 ] }, 2766 ], 2767 }, 2768 2769 'qcheck-core.0.91': { 2770 name: 'QCheck', version: '0.91', opam: 'qcheck-core', 2771 description: 'Property-based testing for OCaml', 2772 universe: U['qcheck-core.0.91'], require: ['qcheck-core'], 2773 sections: [ 2774 { title: 'Generators and Tests', 2775 steps: [ 2776 { code: 'QCheck2.Gen.generate1 (QCheck2.Gen.pair QCheck2.Gen.nat QCheck2.Gen.bool);;', 2777 expect: 'int * bool', description: 'Generate a pair of int and bool' }, 2778 { code: 'let t = QCheck2.Test.make ~name:"assoc" QCheck2.Gen.(triple int int int) (fun (a, b, c) -> (a + b) + c = a + (b + c));;', 2779 expect: 'QCheck2.Test.t', description: 'Addition is associative' }, 2780 { code: 'QCheck2.Test.check_exn t;;', expect: 'unit', 2781 description: 'Property holds' }, 2782 ] }, 2783 ], 2784 }, 2785};