Commits: 11

Setup the project using devenv with flake

new file mode 100644
index 0000000..c5d670d
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+if ! has nix_direnv_version || ! nix_direnv_version 3.1.0; then
+  source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.1.0/direnvrc" "sha256-yMJ2OVMzrFaDPn7q8nCBZFRYpL/f0RcHzhmw/i6btJM="
+fi
+
+export DEVENV_IN_DIRENV_SHELL=true
+
+watch_file flake.nix
+watch_file flake.lock
+if ! use flake . --no-pure-eval; then
+  echo "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2
+fi
new file mode 100644
index 0000000..f7f246e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.devenv
+.direnv/
\ No newline at end of file
new file mode 100644
index 0000000..790b7aa
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,227 @@
+{
+  "nodes": {
+    "cachix": {
+      "inputs": {
+        "devenv": [
+          "devenv"
+        ],
+        "flake-compat": [
+          "devenv",
+          "flake-compat"
+        ],
+        "git-hooks": [
+          "devenv",
+          "git-hooks"
+        ],
+        "nixpkgs": [
+          "devenv",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1760971495,
+        "narHash": "sha256-IwnNtbNVrlZIHh7h4Wz6VP0Furxg9Hh0ycighvL5cZc=",
+        "owner": "cachix",
+        "repo": "cachix",
+        "rev": "c5bfd933d1033672f51a863c47303fc0e093c2d2",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "ref": "latest",
+        "repo": "cachix",
+        "type": "github"
+      }
+    },
+    "devenv": {
+      "inputs": {
+        "cachix": "cachix",
+        "flake-compat": "flake-compat",
+        "flake-parts": "flake-parts",
+        "git-hooks": "git-hooks",
+        "nix": "nix",
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1762889687,
+        "narHash": "sha256-oKvHfeYDZ0LfuHSaFLA0w/dfZ9R6C5W8pCGUjUWawGI=",
+        "owner": "cachix",
+        "repo": "devenv",
+        "rev": "3b4fb549962342c928aae1bbea3a13f0eeed2703",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "repo": "devenv",
+        "type": "github"
+      }
+    },
+    "flake-compat": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1761588595,
+        "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
+        "type": "github"
+      },
+      "original": {
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "type": "github"
+      }
+    },
+    "flake-parts": {
+      "inputs": {
+        "nixpkgs-lib": [
+          "devenv",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1760948891,
+        "narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=",
+        "owner": "hercules-ci",
+        "repo": "flake-parts",
+        "rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "flake-parts",
+        "type": "github"
+      }
+    },
+    "git-hooks": {
+      "inputs": {
+        "flake-compat": [
+          "devenv",
+          "flake-compat"
+        ],
+        "gitignore": "gitignore",
+        "nixpkgs": [
+          "devenv",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1760663237,
+        "narHash": "sha256-BflA6U4AM1bzuRMR8QqzPXqh8sWVCNDzOdsxXEguJIc=",
+        "owner": "cachix",
+        "repo": "git-hooks.nix",
+        "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "repo": "git-hooks.nix",
+        "type": "github"
+      }
+    },
+    "gitignore": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "git-hooks",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1709087332,
+        "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "type": "github"
+      }
+    },
+    "nix": {
+      "inputs": {
+        "flake-compat": [
+          "devenv",
+          "flake-compat"
+        ],
+        "flake-parts": [
+          "devenv",
+          "flake-parts"
+        ],
+        "git-hooks-nix": [
+          "devenv",
+          "git-hooks"
+        ],
+        "nixpkgs": [
+          "devenv",
+          "nixpkgs"
+        ],
+        "nixpkgs-23-11": [
+          "devenv"
+        ],
+        "nixpkgs-regression": [
+          "devenv"
+        ]
+      },
+      "locked": {
+        "lastModified": 1761648602,
+        "narHash": "sha256-H97KSB/luq/aGobKRuHahOvT1r7C03BgB6D5HBZsbN8=",
+        "owner": "cachix",
+        "repo": "nix",
+        "rev": "3e5644da6830ef65f0a2f7ec22830c46285bfff6",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "ref": "devenv-2.30.6",
+        "repo": "nix",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1761313199,
+        "narHash": "sha256-wCIACXbNtXAlwvQUo1Ed++loFALPjYUA3dpcUJiXO44=",
+        "owner": "cachix",
+        "repo": "devenv-nixpkgs",
+        "rev": "d1c30452ebecfc55185ae6d1c983c09da0c274ff",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "ref": "rolling",
+        "repo": "devenv-nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "devenv": "devenv",
+        "nixpkgs": "nixpkgs",
+        "systems": "systems"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
new file mode 100644
index 0000000..f90aba3
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,42 @@
+{
+  inputs = {
+    nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling";
+    systems.url = "github:nix-systems/default";
+    devenv.url = "github:cachix/devenv";
+    devenv.inputs.nixpkgs.follows = "nixpkgs";
+  };
+
+  nixConfig = {
+    extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
+    extra-substituters = "https://devenv.cachix.org";
+  };
+
+  outputs = { self, nixpkgs, devenv, systems, ... } @ inputs:
+    let
+      forEachSystem = nixpkgs.lib.genAttrs (import systems);
+    in
+    {
+      devShells = forEachSystem
+        (system:
+          let
+            pkgs = nixpkgs.legacyPackages.${system};
+          in
+          {
+            default = devenv.lib.mkShell {
+              inherit inputs pkgs;
+              modules = [
+                {
+                  # https://devenv.sh/reference/options/
+                  packages = [ pkgs.hello ];
+
+                  enterShell = ''
+                    hello
+                  '';
+
+                  processes.hello.exec = "hello";
+                }
+              ];
+            };
+          });
+    };
+}

Setup Rust development environment

index f7f246e..98507b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,6 @@
=.devenv
-.direnv/
\ No newline at end of file
+.direnv/
+
+# Added by cargo
+
+/target
new file mode 100644
index 0000000..ee2c2d5
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "tad-better-behavior"
+version = "0.1.0"
new file mode 100644
index 0000000..048938e
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,6 @@
+[package]
+name = "tad-better-behavior"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
index f90aba3..0560c1e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -27,13 +27,9 @@
=              modules = [
=                {
=                  # https://devenv.sh/reference/options/
-                  packages = [ pkgs.hello ];
=
-                  enterShell = ''
-                    hello
-                  '';
-
-                  processes.hello.exec = "hello";
+                  languages.rust.enable = true;
+                  packages = [];
=                }
=              ];
=            };
new file mode 100644
index 0000000..e7a11a9
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,3 @@
+fn main() {
+    println!("Hello, world!");
+}

Setup CLI parsing with Clap

index ee2c2d5..488461d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,185 @@
=# It is not intended for manual editing.
=version = 4
=
+[[package]]
+name = "anstream"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.110"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
=[[package]]
=name = "tad-better-behavior"
=version = "0.1.0"
+dependencies = [
+ "clap",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
index 048938e..ead47b9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,3 +4,4 @@ version = "0.1.0"
=edition = "2024"
=
=[dependencies]
+clap = { version = "4.5.51", features = ["derive"] }
index e7a11a9..414bc9c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,15 @@
+use clap::Parser;
+use std::path::PathBuf;
+
+#[derive(Parser)]
+#[command(version, about, long_about=None)]
+struct Cli {
+    #[arg(value_name = "SPEC PATH", default_value = "./spec/")]
+    input: PathBuf,
+}
+
=fn main() {
-    println!("Hello, world!");
+    let cli = Cli::parse();
+
+    println!("Reading specifications from {}", cli.input.display());
=}

Set the name of main binary to tbb

Short for Tad Better Behavior!

index ead47b9..3386de1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,3 +5,7 @@ edition = "2024"
=
=[dependencies]
=clap = { version = "4.5.51", features = ["derive"] }
+
+[[bin]]
+name = "tbb"
+path = "src/main.rs"

Describe the input option

index 414bc9c..a606887 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,6 +4,7 @@ use std::path::PathBuf;
=#[derive(Parser)]
=#[command(version, about, long_about=None)]
=struct Cli {
+    /// A directory or a markdown file with specs to evaluate
=    #[arg(value_name = "SPEC PATH", default_value = "./spec/")]
=    input: PathBuf,
=}

Write a basic sample specification

new file mode 100644
index 0000000..163ee32
--- /dev/null
+++ b/samples/basic.md
@@ -0,0 +1,29 @@
+---
+interpretter: "python samples/basic.py"
+---
+
+# Basic BDD suite
+
+This suite contains several simple **scenarios**. Use it as a reference to get started with Tad Better Behavior.
+
+Scenarios are delimited by 2nd level heading (`## Heading` in markdown). Each bullet point inside a scenario defines a **step**. 
+
+
+For each scenario the **interpreter** program (see the front-matter above) will be started. For every step this program will be passed a JSON object via `stdin`. The object will contain the description of the step. An interpretter is expected to print a JSON object with the result of evaluation on it's `stdout`.
+
+
+## Arithmetic
+
+Content outside of bullet points (like this) won't have any effect on the program. You can user it as a humane description of the scenario, or for any other purpose.
+
+  * Add `7` and `5` to get `12`.
+  * Divide `8` by `4` to get `2`
+  * Subtract `7` from `5` to get `-2`
+
+
+## Text
+
+  * The word `blocks` has `6` characters
+  * There are `3` `r`s in the word `strawberry`
+  * The reverse of `abc` is `cba`
+  * There are `2` `o`s in the word `boost`

Distinguish between file and directory inputs

Handle errors.

index a606887..c6a8eae 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,8 +9,19 @@ struct Cli {
=    input: PathBuf,
=}
=
-fn main() {
+fn main() -> Result<(), Box<dyn std::error::Error>> {
=    let cli = Cli::parse();
=
=    println!("Reading specifications from {}", cli.input.display());
+
+    let input = cli.input.canonicalize()?;
+    if input.is_dir() {
+        println!("Input is a directory. Looking for markdown files...");
+        Ok(())
+    } else if input.is_file() {
+        println!("Input is a file. Reading...");
+        Ok(())
+    } else {
+        Err(format!("The {} is neither a file nor directory", input.display()).into())
+    }
=}

Read the spec input and print it back

index 488461d..ed46f99 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -98,6 +98,12 @@ version = "1.0.4"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
=
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
=[[package]]
=name = "heck"
=version = "0.5.0"
@@ -156,6 +162,7 @@ name = "tad-better-behavior"
=version = "0.1.0"
=dependencies = [
= "clap",
+ "glob",
=]
=
=[[package]]
index 3386de1..3e2476d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,7 @@ edition = "2024"
=
=[dependencies]
=clap = { version = "4.5.51", features = ["derive"] }
+glob = "0.3.3"
=
=[[bin]]
=name = "tbb"
index c6a8eae..66b20da 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,6 @@
=use clap::Parser;
+use glob::glob;
+use std::error::Error;
=use std::path::PathBuf;
=
=#[derive(Parser)]
@@ -9,19 +11,33 @@ struct Cli {
=    input: PathBuf,
=}
=
-fn main() -> Result<(), Box<dyn std::error::Error>> {
+fn main() -> Result<(), Box<dyn Error>> {
=    let cli = Cli::parse();
=
=    println!("Reading specifications from {}", cli.input.display());
=
=    let input = cli.input.canonicalize()?;
=    if input.is_dir() {
-        println!("Input is a directory. Looking for markdown files...");
+        println!(
+            "The {} is a directory. Looking for markdown files...",
+            input.display()
+        );
+        let pattern = format!("{}/**/*.md", input.display());
+        for path in glob(&pattern)? {
+            process_spec(path?)?;
+        }
=        Ok(())
=    } else if input.is_file() {
-        println!("Input is a file. Reading...");
-        Ok(())
+        println!("The {} is a file. Reading...", input.display());
+        process_spec(input)
=    } else {
=        Err(format!("The {} is neither a file nor directory", input.display()).into())
=    }
=}
+
+fn process_spec(input: PathBuf) -> Result<(), Box<dyn Error>> {
+    println!("Reading {}", input.display());
+    let markdown = std::fs::read_to_string(input)?;
+    println!("Content:\n\n{}", markdown);
+    Ok(())
+}

Parse input to mdast

index ed46f99..d270297 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -116,6 +116,15 @@ version = "1.70.2"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
=
+[[package]]
+name = "markdown"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb"
+dependencies = [
+ "unicode-id",
+]
+
=[[package]]
=name = "once_cell_polyfill"
=version = "1.70.2"
@@ -163,8 +172,15 @@ version = "0.1.0"
=dependencies = [
= "clap",
= "glob",
+ "markdown",
=]
=
+[[package]]
+name = "unicode-id"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580"
+
=[[package]]
=name = "unicode-ident"
=version = "1.0.22"
index 3e2476d..0ee1226 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2024"
=[dependencies]
=clap = { version = "4.5.51", features = ["derive"] }
=glob = "0.3.3"
+markdown = "1.0.0"
=
=[[bin]]
=name = "tbb"
index 163ee32..134df0d 100644
--- a/samples/basic.md
+++ b/samples/basic.md
@@ -8,7 +8,6 @@ This suite contains several simple **scenarios**. Use it as a reference to get s
=
=Scenarios are delimited by 2nd level heading (`## Heading` in markdown). Each bullet point inside a scenario defines a **step**. 
=
-
=For each scenario the **interpreter** program (see the front-matter above) will be started. For every step this program will be passed a JSON object via `stdin`. The object will contain the description of the step. An interpretter is expected to print a JSON object with the result of evaluation on it's `stdout`.
=
=
index 66b20da..b884fb4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,6 @@
=use clap::Parser;
=use glob::glob;
+use markdown;
=use std::error::Error;
=use std::path::PathBuf;
=
@@ -37,7 +38,18 @@ fn main() -> Result<(), Box<dyn Error>> {
=
=fn process_spec(input: PathBuf) -> Result<(), Box<dyn Error>> {
=    println!("Reading {}", input.display());
-    let markdown = std::fs::read_to_string(input)?;
-    println!("Content:\n\n{}", markdown);
+    let md = std::fs::read_to_string(input)?;
+    let mdast = markdown::to_mdast(
+        &md,
+        &markdown::ParseOptions {
+            constructs: markdown::Constructs {
+                frontmatter: true,
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+    )
+    .map_err(|message| message.to_string())?;
+    println!("Content:\n\n{:#?}", mdast);
=    Ok(())
=}

Define structs (Spec, Scenario, Step)

Also extract interpretter from a front-matter using serde_yaml.

index d270297..efecadd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -98,24 +98,52 @@ version = "1.0.4"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
=
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
=[[package]]
=name = "glob"
=version = "0.3.3"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
=
+[[package]]
+name = "hashbrown"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
+
=[[package]]
=name = "heck"
=version = "0.5.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
=
+[[package]]
+name = "indexmap"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
=[[package]]
=name = "is_terminal_polyfill"
=version = "1.70.2"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
=
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
=[[package]]
=name = "markdown"
=version = "1.0.0"
@@ -149,6 +177,55 @@ dependencies = [
= "proc-macro2",
=]
=
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
=[[package]]
=name = "strsim"
=version = "0.11.1"
@@ -173,6 +250,8 @@ dependencies = [
= "clap",
= "glob",
= "markdown",
+ "serde",
+ "serde_yaml",
=]
=
=[[package]]
@@ -187,6 +266,12 @@ version = "1.0.22"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
=
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
=[[package]]
=name = "utf8parse"
=version = "0.2.2"
index 0ee1226..0511019 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,8 @@ edition = "2024"
=clap = { version = "4.5.51", features = ["derive"] }
=glob = "0.3.3"
=markdown = "1.0.0"
+serde = { version = "1.0.228", features = ["serde_derive"] }
+serde_yaml = "0.9.34"
=
=[[bin]]
=name = "tbb"
index b884fb4..0e98912 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,7 @@
=use clap::Parser;
=use glob::glob;
=use markdown;
+use serde::Deserialize;
=use std::error::Error;
=use std::path::PathBuf;
=
@@ -36,6 +37,58 @@ fn main() -> Result<(), Box<dyn Error>> {
=    }
=}
=
+#[derive(Debug)]
+struct Spec {
+    title: String,
+    interpretter: String,
+    scenarios: Vec<Scenario>,
+}
+
+#[derive(Debug)]
+struct Scenario {
+    title: String,
+    steps: Vec<Step>,
+}
+
+#[derive(Debug)]
+struct Step {
+    variant: String,
+    arguments: Vec<String>,
+}
+
+#[derive(Deserialize)]
+struct FrontMatter {
+    interpretter: String,
+}
+
+impl TryFrom<markdown::mdast::Node> for Spec {
+    type Error = Box<dyn Error>;
+
+    fn try_from(mdast: markdown::mdast::Node) -> Result<Self, Self::Error> {
+        // Find the YAML frontmatter and extract interpretter field
+        let children = mdast.children().ok_or("The given node has no children.")?;
+        let first = children
+            .first()
+            .ok_or("There is no first child in the given node.")?;
+
+        let markdown::mdast::Node::Yaml(markdown::mdast::Yaml { value: yaml, .. }) = first else {
+            return Err("First child is not a YAML front matter.".into());
+        };
+        let frontmatter: FrontMatter = serde_yaml::from_str(yaml)?;
+
+        // TODO: Find h1 and use it as title (make sure there's only one)
+
+        // TODO: Split into sections, each starting at h2
+        // TODO: Convert each section into a scenario (section::try_into())
+
+        Ok(Self {
+            title: "Spec title".into(),
+            interpretter: frontmatter.interpretter,
+            scenarios: [].into(),
+        })
+    }
+}
+
=fn process_spec(input: PathBuf) -> Result<(), Box<dyn Error>> {
=    println!("Reading {}", input.display());
=    let md = std::fs::read_to_string(input)?;
@@ -51,5 +104,8 @@ fn process_spec(input: PathBuf) -> Result<(), Box<dyn Error>> {
=    )
=    .map_err(|message| message.to_string())?;
=    println!("Content:\n\n{:#?}", mdast);
+
+    let spec = Spec::try_from(mdast)?;
+    println!("Spec:\n\n{:#?}", spec);
=    Ok(())
=}

Fix a typo

Spelling hard

index 134df0d..95a7704 100644
--- a/samples/basic.md
+++ b/samples/basic.md
@@ -1,5 +1,5 @@
=---
-interpretter: "python samples/basic.py"
+interpreter: "python samples/basic.py"
=---
=
=# Basic BDD suite
@@ -8,7 +8,7 @@ This suite contains several simple **scenarios**. Use it as a reference to get s
=
=Scenarios are delimited by 2nd level heading (`## Heading` in markdown). Each bullet point inside a scenario defines a **step**. 
=
-For each scenario the **interpreter** program (see the front-matter above) will be started. For every step this program will be passed a JSON object via `stdin`. The object will contain the description of the step. An interpretter is expected to print a JSON object with the result of evaluation on it's `stdout`.
+For each scenario the **interpreter** program (see the front-matter above) will be started. For every step this program will be passed a JSON object via `stdin`. The object will contain the description of the step. An interpreter is expected to print a JSON object with the result of evaluation on it's `stdout`.
=
=
=## Arithmetic
index 0e98912..151721e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -40,7 +40,7 @@ fn main() -> Result<(), Box<dyn Error>> {
=#[derive(Debug)]
=struct Spec {
=    title: String,
-    interpretter: String,
+    interpreter: String,
=    scenarios: Vec<Scenario>,
=}
=
@@ -58,14 +58,14 @@ struct Step {
=
=#[derive(Deserialize)]
=struct FrontMatter {
-    interpretter: String,
+    interpreter: String,
=}
=
=impl TryFrom<markdown::mdast::Node> for Spec {
=    type Error = Box<dyn Error>;
=
=    fn try_from(mdast: markdown::mdast::Node) -> Result<Self, Self::Error> {
-        // Find the YAML frontmatter and extract interpretter field
+        // Find the YAML front-matter and extract the interpreter field
=        let children = mdast.children().ok_or("The given node has no children.")?;
=        let first = children
=            .first()
@@ -83,7 +83,7 @@ impl TryFrom<markdown::mdast::Node> for Spec {
=
=        Ok(Self {
=            title: "Spec title".into(),
-            interpretter: frontmatter.interpretter,
+            interpreter: frontmatter.interpreter,
=            scenarios: [].into(),
=        })
=    }