diff --git a/.editorconfig b/.editorconfig
index 8deec94..9e0ba60 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -6,7 +6,7 @@ indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
-[*.{json,yaml,yml}]
+[*.{html,js,json,yaml,yml}]
indent_size = 2
[Justfile]
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 2c63c08..e2bbf0b 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,2 +1,13 @@
{
+ "editor.quickSuggestions": {
+ "other": "on",
+ "strings": "on"
+ },
+ "files.associations": {
+ "**/templates/**/*.html": "jinja-html",
+ "*.css": "tailwindcss"
+ },
+ "tailwindCSS.includeLanguages": {
+ "jinja-html": "html"
+ }
}
diff --git a/Cargo.lock b/Cargo.lock
index e50501e..98402a1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -86,6 +86,46 @@ version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
+[[package]]
+name = "askama"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
+dependencies = [
+ "askama_derive",
+ "askama_escape",
+ "comrak",
+]
+
+[[package]]
+name = "askama_derive"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a0fc7dcf8bd4ead96b1d36b41df47c14beedf7b0301fc543d8f2384e66a2ec0"
+dependencies = [
+ "askama_parser",
+ "mime",
+ "mime_guess",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.41",
+]
+
+[[package]]
+name = "askama_escape"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
+
+[[package]]
+name = "askama_parser"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c268a96e01a4c47c8c5c2472aaa570707e006a875ea63e819f75474ceedaf7b4"
+dependencies = [
+ "nom",
+]
+
[[package]]
name = "atty"
version = "0.2.14"
@@ -140,6 +180,12 @@ dependencies = [
"serde",
]
+[[package]]
+name = "bumpalo"
+version = "3.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
+
[[package]]
name = "bytes"
version = "1.5.0"
@@ -221,6 +267,21 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+[[package]]
+name = "comrak"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "482aa5695bca086022be453c700a40c02893f1ba7098a2c88351de55341ae894"
+dependencies = [
+ "entities",
+ "memchr",
+ "once_cell",
+ "regex",
+ "slug",
+ "typed-arena",
+ "unicode_categories",
+]
+
[[package]]
name = "condtype"
version = "1.3.0"
@@ -290,6 +351,12 @@ dependencies = [
"powerfmt",
]
+[[package]]
+name = "deunicode"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a1abaf4d861455be59f64fd2b55606cb151fce304ede7165f410243ce96bde6"
+
[[package]]
name = "directories"
version = "5.0.1"
@@ -346,6 +413,12 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
+[[package]]
+name = "entities"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
+
[[package]]
name = "erased-serde"
version = "0.3.31"
@@ -679,6 +752,28 @@ dependencies = [
"libmimalloc-sys",
]
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
[[package]]
name = "miniz_oxide"
version = "0.7.1"
@@ -694,6 +789,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
[[package]]
name = "num_threads"
version = "0.1.6"
@@ -871,6 +976,18 @@ dependencies = [
"thiserror",
]
+[[package]]
+name = "regex"
+version = "1.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
[[package]]
name = "regex-automata"
version = "0.4.3"
@@ -1001,6 +1118,16 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597"
+[[package]]
+name = "slug"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4"
+dependencies = [
+ "deunicode",
+ "wasm-bindgen",
+]
+
[[package]]
name = "smallvec"
version = "1.11.2"
@@ -1087,6 +1214,19 @@ dependencies = [
"syn 2.0.41",
]
+[[package]]
+name = "stef-doc"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "askama",
+ "glob",
+ "heck",
+ "insta",
+ "stef-meta",
+ "stef-parser",
+]
+
[[package]]
name = "stef-go"
version = "0.1.0"
@@ -1117,10 +1257,18 @@ dependencies = [
"serde",
"serde_json",
"stef-compiler",
+ "stef-meta",
"stef-parser",
"time",
]
+[[package]]
+name = "stef-meta"
+version = "0.1.0"
+dependencies = [
+ "stef-parser",
+]
+
[[package]]
name = "stef-parser"
version = "0.1.0"
@@ -1383,6 +1531,21 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+[[package]]
+name = "typed-arena"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
+
+[[package]]
+name = "unicase"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
+dependencies = [
+ "version_check",
+]
+
[[package]]
name = "unicode-bidi"
version = "0.3.14"
@@ -1416,6 +1579,12 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
[[package]]
name = "url"
version = "2.5.0"
@@ -1492,6 +1661,60 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.41",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.41",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
+
[[package]]
name = "winapi"
version = "0.3.9"
diff --git a/Cargo.toml b/Cargo.toml
index c99ce58..e1df026 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ missing_docs = "warn"
[workspace.lints.clippy]
all = "deny"
pedantic = "warn"
+clone_on_ref_ptr = "warn"
[workspace.dependencies]
clap = { version = "4.4.11", features = ["derive", "wrap_help"] }
diff --git a/book/highlight/package.json b/book/highlight/package.json
index 315aa08..cc8f966 100644
--- a/book/highlight/package.json
+++ b/book/highlight/package.json
@@ -12,6 +12,6 @@
},
"devDependencies": {
"@biomejs/biome": "^1.4.1",
- "esbuild": "^0.19.9"
+ "esbuild": "^0.19.10"
}
}
diff --git a/bun.lockb b/bun.lockb
index f05e99a..15f1f18 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/crates/stef-build/src/definition.rs b/crates/stef-build/src/definition.rs
index d412da8..9e39a89 100644
--- a/crates/stef-build/src/definition.rs
+++ b/crates/stef-build/src/definition.rs
@@ -186,7 +186,11 @@ fn compile_const(
}
}
-fn compile_import(Import { segments, element }: &Import<'_>) -> TokenStream {
+fn compile_import(
+ Import {
+ segments, element, ..
+ }: &Import<'_>,
+) -> TokenStream {
let segments = segments.iter().enumerate().map(|(i, segment)| {
let segment = Ident::new(segment.get(), Span::call_site());
if i > 0 {
diff --git a/crates/stef-build/tests/snapshots/compiler__compile@mixed.stef.snap b/crates/stef-build/tests/snapshots/compiler__compile@mixed.stef.snap
new file mode 100644
index 0000000..5cc3a8b
--- /dev/null
+++ b/crates/stef-build/tests/snapshots/compiler__compile@mixed.stef.snap
@@ -0,0 +1,665 @@
+---
+source: crates/stef-build/tests/compiler.rs
+description: "/// Basic user information.\n///\n/// Uses various other `structs` and `enums` to compose the information\n/// in a **type safe** way.\nstruct User {\n name: FullName @1,\n /// Physical address, might not be specified by the user.\n address: option
@2,\n age: u8 @3,\n birthday: birthday::DayOfBirth @4,\n}\n\n/// Full name of a user.\nstruct FullName {\n first: string @1,\n middle: option @2,\n last: string @3,\n}\n\n/// Simple alias for convenience.\n///\n/// - Might be easier to remember.\n/// - Often referenced as this.\ntype Name = FullName;\n\nstruct Address {\n /// Street name.\n street: string @1,\n /// Number of the house in the street.\n house_no: HouseNumber @2,\n city: string @3,\n}\n\n/// The number on the house.\n///\n/// More details can be found at [Wikipedia](https://en.wikipedia.org/wiki/House_numbering).\nenum HouseNumber {\n /// Digit only number.\n Digit(u16 @1) @1,\n /// Mixed _number_ with characters like `1a`.\n Text(string @1) @2,\n}\n\n/// Probably the max age of a human, currently.\nconst MAX_AGE: u8 = 120;\n\n\n/// Details for defining birthdays.\nmod birthday {\n /// As the name suggests, specifies details about birthdays.\n enum DayOfBirth {\n Specific {\n year: u16 @1,\n month: Month @2,\n day: u8 @3,\n } @1,\n /// The user didn't want to say.\n Secret {\n /// Optional info from the user about why they didn't want to\n /// reveal their birthday.\n reason: option @1,\n } @2,\n /// We never asked and nobody knows.\n Unknown @3,\n }\n\n /// Let's assume we only have details of people born **after** this year.\n const MIN_YEAR: u16 = 1900;\n\n /// Absolute maximum for a day, but might be even less depending\n /// on the month.\n const MAX_DAY: u8 = 31;\n\n /// The month of the year.\n enum Month {\n January @1,\n February @2,\n March @3,\n April @4,\n May @5,\n June @6,\n July @7,\n August @8,\n September @9,\n October @10,\n November @11,\n December @12,\n }\n}"
+input_file: crates/stef-parser/tests/inputs/mixed.stef
+---
+#[allow(unused_imports)]
+use ::stef::buf::{Decode, Encode, Size};
+/// Basic user information.
+///
+/// Uses various other `structs` and `enums` to compose the information
+/// in a **type safe** way.
+#[derive(Clone, Debug, PartialEq)]
+#[allow(clippy::module_name_repetitions, clippy::option_option)]
+pub struct User {
+ pub name: FullName,
+ /// Physical address, might not be specified by the user.
+ pub address: Option,
+ pub age: u8,
+ pub birthday: birthday::DayOfBirth,
+}
+#[automatically_derived]
+impl ::stef::Encode for User {
+ #[allow(
+ clippy::borrow_deref_ref,
+ clippy::explicit_auto_deref,
+ clippy::needless_borrow,
+ clippy::too_many_lines,
+ )]
+ fn encode(&self, w: &mut impl ::stef::BufMut) {
+ ::stef::buf::encode_field(
+ w,
+ 1,
+ |w| {
+ (self.name).encode(w);
+ },
+ );
+ ::stef::buf::encode_field_option(
+ w,
+ 2,
+ &self.address,
+ |w, v| {
+ (v).encode(w);
+ },
+ );
+ ::stef::buf::encode_field(
+ w,
+ 3,
+ |w| {
+ ::stef::buf::encode_u8(w, self.age);
+ },
+ );
+ ::stef::buf::encode_field(
+ w,
+ 4,
+ |w| {
+ (self.birthday).encode(w);
+ },
+ );
+ ::stef::buf::encode_u32(w, ::stef::buf::END_MARKER);
+ }
+}
+#[automatically_derived]
+impl ::stef::Decode for User {
+ #[allow(clippy::type_complexity, clippy::too_many_lines)]
+ fn decode(r: &mut impl ::stef::Buf) -> ::stef::buf::Result {
+ let mut name: Option = None;
+ let mut address: Option = None;
+ let mut age: Option = None;
+ let mut birthday: Option = None;
+ loop {
+ match ::stef::buf::decode_id(r)? {
+ ::stef::buf::END_MARKER => break,
+ 1 => name = Some(FullName::decode(r)?),
+ 2 => address = Some(Address::decode(r)?),
+ 3 => age = Some(::stef::buf::decode_u8(r)?),
+ 4 => birthday = Some(birthday::DayOfBirth::decode(r)?),
+ _ => continue,
+ }
+ }
+ Ok(Self {
+ name: name
+ .ok_or(::stef::buf::Error::MissingField {
+ id: 1,
+ name: Some("name"),
+ })?,
+ address,
+ age: age
+ .ok_or(::stef::buf::Error::MissingField {
+ id: 3,
+ name: Some("age"),
+ })?,
+ birthday: birthday
+ .ok_or(::stef::buf::Error::MissingField {
+ id: 4,
+ name: Some("birthday"),
+ })?,
+ })
+ }
+}
+#[automatically_derived]
+impl ::stef::buf::Size for User {
+ #[allow(
+ clippy::borrow_deref_ref,
+ clippy::explicit_auto_deref,
+ clippy::needless_borrow,
+ clippy::too_many_lines,
+ )]
+ fn size(&self) -> usize {
+ ::stef::buf::size_field(1, || { (self.name).size() })
+ + ::stef::buf::size_field_option(
+ 2,
+ self.address.as_ref(),
+ |v| { (v).size() },
+ ) + ::stef::buf::size_field(3, || { ::stef::buf::size_u8(self.age) })
+ + ::stef::buf::size_field(4, || { (self.birthday).size() })
+ + ::stef::buf::size_u32(::stef::buf::END_MARKER)
+ }
+}
+/// Full name of a user.
+#[derive(Clone, Debug, PartialEq)]
+#[allow(clippy::module_name_repetitions, clippy::option_option)]
+pub struct FullName {
+ pub first: String,
+ pub middle: Option,
+ pub last: String,
+}
+#[automatically_derived]
+impl ::stef::Encode for FullName {
+ #[allow(
+ clippy::borrow_deref_ref,
+ clippy::explicit_auto_deref,
+ clippy::needless_borrow,
+ clippy::too_many_lines,
+ )]
+ fn encode(&self, w: &mut impl ::stef::BufMut) {
+ ::stef::buf::encode_field(
+ w,
+ 1,
+ |w| {
+ ::stef::buf::encode_string(w, &self.first);
+ },
+ );
+ ::stef::buf::encode_field_option(
+ w,
+ 2,
+ &self.middle,
+ |w, v| {
+ ::stef::buf::encode_string(w, &v);
+ },
+ );
+ ::stef::buf::encode_field(
+ w,
+ 3,
+ |w| {
+ ::stef::buf::encode_string(w, &self.last);
+ },
+ );
+ ::stef::buf::encode_u32(w, ::stef::buf::END_MARKER);
+ }
+}
+#[automatically_derived]
+impl ::stef::Decode for FullName {
+ #[allow(clippy::type_complexity, clippy::too_many_lines)]
+ fn decode(r: &mut impl ::stef::Buf) -> ::stef::buf::Result {
+ let mut first: Option = None;
+ let mut middle: Option = None;
+ let mut last: Option = None;
+ loop {
+ match ::stef::buf::decode_id(r)? {
+ ::stef::buf::END_MARKER => break,
+ 1 => first = Some(::stef::buf::decode_string(r)?),
+ 2 => middle = Some(::stef::buf::decode_string(r)?),
+ 3 => last = Some(::stef::buf::decode_string(r)?),
+ _ => continue,
+ }
+ }
+ Ok(Self {
+ first: first
+ .ok_or(::stef::buf::Error::MissingField {
+ id: 1,
+ name: Some("first"),
+ })?,
+ middle,
+ last: last
+ .ok_or(::stef::buf::Error::MissingField {
+ id: 3,
+ name: Some("last"),
+ })?,
+ })
+ }
+}
+#[automatically_derived]
+impl ::stef::buf::Size for FullName {
+ #[allow(
+ clippy::borrow_deref_ref,
+ clippy::explicit_auto_deref,
+ clippy::needless_borrow,
+ clippy::too_many_lines,
+ )]
+ fn size(&self) -> usize {
+ ::stef::buf::size_field(1, || { ::stef::buf::size_string(&self.first) })
+ + ::stef::buf::size_field_option(
+ 2,
+ self.middle.as_ref(),
+ |v| { ::stef::buf::size_string(&v) },
+ ) + ::stef::buf::size_field(3, || { ::stef::buf::size_string(&self.last) })
+ + ::stef::buf::size_u32(::stef::buf::END_MARKER)
+ }
+}
+/// Simple alias for convenience.
+///
+/// - Might be easier to remember.
+/// - Often referenced as this.
+#[allow(dead_code, clippy::module_name_repetitions, clippy::option_option)]
+pub type Name = FullName;
+#[derive(Clone, Debug, PartialEq)]
+#[allow(clippy::module_name_repetitions, clippy::option_option)]
+pub struct Address {
+ /// Street name.
+ pub street: String,
+ /// Number of the house in the street.
+ pub house_no: HouseNumber,
+ pub city: String,
+}
+#[automatically_derived]
+impl ::stef::Encode for Address {
+ #[allow(
+ clippy::borrow_deref_ref,
+ clippy::explicit_auto_deref,
+ clippy::needless_borrow,
+ clippy::too_many_lines,
+ )]
+ fn encode(&self, w: &mut impl ::stef::BufMut) {
+ ::stef::buf::encode_field(
+ w,
+ 1,
+ |w| {
+ ::stef::buf::encode_string(w, &self.street);
+ },
+ );
+ ::stef::buf::encode_field(
+ w,
+ 2,
+ |w| {
+ (self.house_no).encode(w);
+ },
+ );
+ ::stef::buf::encode_field(
+ w,
+ 3,
+ |w| {
+ ::stef::buf::encode_string(w, &self.city);
+ },
+ );
+ ::stef::buf::encode_u32(w, ::stef::buf::END_MARKER);
+ }
+}
+#[automatically_derived]
+impl ::stef::Decode for Address {
+ #[allow(clippy::type_complexity, clippy::too_many_lines)]
+ fn decode(r: &mut impl ::stef::Buf) -> ::stef::buf::Result {
+ let mut street: Option = None;
+ let mut house_no: Option = None;
+ let mut city: Option = None;
+ loop {
+ match ::stef::buf::decode_id(r)? {
+ ::stef::buf::END_MARKER => break,
+ 1 => street = Some(::stef::buf::decode_string(r)?),
+ 2 => house_no = Some(HouseNumber::decode(r)?),
+ 3 => city = Some(::stef::buf::decode_string(r)?),
+ _ => continue,
+ }
+ }
+ Ok(Self {
+ street: street
+ .ok_or(::stef::buf::Error::MissingField {
+ id: 1,
+ name: Some("street"),
+ })?,
+ house_no: house_no
+ .ok_or(::stef::buf::Error::MissingField {
+ id: 2,
+ name: Some("house_no"),
+ })?,
+ city: city
+ .ok_or(::stef::buf::Error::MissingField {
+ id: 3,
+ name: Some("city"),
+ })?,
+ })
+ }
+}
+#[automatically_derived]
+impl ::stef::buf::Size for Address {
+ #[allow(
+ clippy::borrow_deref_ref,
+ clippy::explicit_auto_deref,
+ clippy::needless_borrow,
+ clippy::too_many_lines,
+ )]
+ fn size(&self) -> usize {
+ ::stef::buf::size_field(1, || { ::stef::buf::size_string(&self.street) })
+ + ::stef::buf::size_field(2, || { (self.house_no).size() })
+ + ::stef::buf::size_field(3, || { ::stef::buf::size_string(&self.city) })
+ + ::stef::buf::size_u32(::stef::buf::END_MARKER)
+ }
+}
+/// The number on the house.
+///
+/// More details can be found at [Wikipedia](https://en.wikipedia.org/wiki/House_numbering).
+#[derive(Clone, Debug, PartialEq)]
+#[allow(clippy::module_name_repetitions, clippy::option_option)]
+pub enum HouseNumber {
+ /// Digit only number.
+ Digit(u16),
+ /// Mixed _number_ with characters like `1a`.
+ Text(String),
+}
+#[automatically_derived]
+impl ::stef::Encode for HouseNumber {
+ #[allow(
+ clippy::borrow_deref_ref,
+ clippy::semicolon_if_nothing_returned,
+ clippy::too_many_lines,
+ )]
+ fn encode(&self, w: &mut impl ::stef::BufMut) {
+ match self {
+ Self::Digit(n0) => {
+ ::stef::buf::encode_id(w, 1);
+ ::stef::buf::encode_field(w, 1, |w| { ::stef::buf::encode_u16(w, *n0) });
+ ::stef::buf::encode_u32(w, ::stef::buf::END_MARKER);
+ }
+ Self::Text(n0) => {
+ ::stef::buf::encode_id(w, 2);
+ ::stef::buf::encode_field(
+ w,
+ 1,
+ |w| { ::stef::buf::encode_string(w, &*n0) },
+ );
+ ::stef::buf::encode_u32(w, ::stef::buf::END_MARKER);
+ }
+ }
+ }
+}
+#[automatically_derived]
+impl ::stef::Decode for HouseNumber {
+ #[allow(clippy::too_many_lines)]
+ fn decode(r: &mut impl ::stef::Buf) -> ::stef::buf::Result {
+ match ::stef::buf::decode_id(r)? {
+ 1 => {
+ let mut n0: Option = None;
+ loop {
+ match ::stef::buf::decode_id(r)? {
+ ::stef::buf::END_MARKER => break,
+ 1 => n0 = Some(::stef::buf::decode_u16(r)?),
+ _ => continue,
+ }
+ }
+ Ok(
+ Self::Digit(
+ n0
+ .ok_or(::stef::buf::Error::MissingField {
+ id: 1,
+ name: None,
+ })?,
+ ),
+ )
+ }
+ 2 => {
+ let mut n0: Option = None;
+ loop {
+ match ::stef::buf::decode_id(r)? {
+ ::stef::buf::END_MARKER => break,
+ 1 => n0 = Some(::stef::buf::decode_string(r)?),
+ _ => continue,
+ }
+ }
+ Ok(
+ Self::Text(
+ n0
+ .ok_or(::stef::buf::Error::MissingField {
+ id: 1,
+ name: None,
+ })?,
+ ),
+ )
+ }
+ id => Err(::stef::buf::Error::UnknownVariant(id)),
+ }
+ }
+}
+#[automatically_derived]
+impl ::stef::buf::Size for HouseNumber {
+ #[allow(
+ clippy::borrow_deref_ref,
+ clippy::semicolon_if_nothing_returned,
+ clippy::too_many_lines,
+ )]
+ fn size(&self) -> usize {
+ match self {
+ Self::Digit(n0) => {
+ ::stef::buf::size_id(1)
+ + ::stef::buf::size_field(1, || { ::stef::buf::size_u16(*n0) })
+ + ::stef::buf::size_u32(::stef::buf::END_MARKER)
+ }
+ Self::Text(n0) => {
+ ::stef::buf::size_id(2)
+ + ::stef::buf::size_field(1, || { ::stef::buf::size_string(&*n0) })
+ + ::stef::buf::size_u32(::stef::buf::END_MARKER)
+ }
+ }
+ }
+}
+/// Probably the max age of a human, currently.
+#[allow(dead_code)]
+pub const MAX_AGE: u8 = 120;
+/// Details for defining birthdays.
+pub mod birthday {
+ #[allow(unused_imports)]
+ use ::stef::buf::{Decode, Encode, Size};
+ /// As the name suggests, specifies details about birthdays.
+ #[derive(Clone, Debug, PartialEq)]
+ #[allow(clippy::module_name_repetitions, clippy::option_option)]
+ pub enum DayOfBirth {
+ Specific { year: u16, month: Month, day: u8 },
+ /// The user didn't want to say.
+ Secret {
+ /// Optional info from the user about why they didn't want to
+ /// reveal their birthday.
+ reason: Option,
+ },
+ /// We never asked and nobody knows.
+ Unknown,
+ }
+ #[automatically_derived]
+ impl ::stef::Encode for DayOfBirth {
+ #[allow(
+ clippy::borrow_deref_ref,
+ clippy::semicolon_if_nothing_returned,
+ clippy::too_many_lines,
+ )]
+ fn encode(&self, w: &mut impl ::stef::BufMut) {
+ match self {
+ Self::Specific { year, month, day } => {
+ ::stef::buf::encode_id(w, 1);
+ ::stef::buf::encode_field(
+ w,
+ 1,
+ |w| { ::stef::buf::encode_u16(w, *year) },
+ );
+ ::stef::buf::encode_field(w, 2, |w| { (*month).encode(w) });
+ ::stef::buf::encode_field(
+ w,
+ 3,
+ |w| { ::stef::buf::encode_u8(w, *day) },
+ );
+ ::stef::buf::encode_u32(w, ::stef::buf::END_MARKER);
+ }
+ Self::Secret { reason } => {
+ ::stef::buf::encode_id(w, 2);
+ ::stef::buf::encode_field_option(w, 1, &reason);
+ ::stef::buf::encode_u32(w, ::stef::buf::END_MARKER);
+ }
+ Self::Unknown => {
+ ::stef::buf::encode_id(w, 3);
+ }
+ }
+ }
+ }
+ #[automatically_derived]
+ impl ::stef::Decode for DayOfBirth {
+ #[allow(clippy::too_many_lines)]
+ fn decode(r: &mut impl ::stef::Buf) -> ::stef::buf::Result {
+ match ::stef::buf::decode_id(r)? {
+ 1 => {
+ let mut year: Option = None;
+ let mut month: Option = None;
+ let mut day: Option = None;
+ loop {
+ match ::stef::buf::decode_id(r)? {
+ ::stef::buf::END_MARKER => break,
+ 1 => year = Some(::stef::buf::decode_u16(r)?),
+ 2 => month = Some(Month::decode(r)?),
+ 3 => day = Some(::stef::buf::decode_u8(r)?),
+ _ => continue,
+ }
+ }
+ Ok(Self::Specific {
+ year: year
+ .ok_or(::stef::buf::Error::MissingField {
+ id: 1,
+ name: Some("year"),
+ })?,
+ month: month
+ .ok_or(::stef::buf::Error::MissingField {
+ id: 2,
+ name: Some("month"),
+ })?,
+ day: day
+ .ok_or(::stef::buf::Error::MissingField {
+ id: 3,
+ name: Some("day"),
+ })?,
+ })
+ }
+ 2 => {
+ let mut reason: Option = None;
+ loop {
+ match ::stef::buf::decode_id(r)? {
+ ::stef::buf::END_MARKER => break,
+ 1 => reason = Some(::stef::buf::decode_string(r)?),
+ _ => continue,
+ }
+ }
+ Ok(Self::Secret { reason })
+ }
+ 3 => Ok(Self::Unknown),
+ id => Err(::stef::buf::Error::UnknownVariant(id)),
+ }
+ }
+ }
+ #[automatically_derived]
+ impl ::stef::buf::Size for DayOfBirth {
+ #[allow(
+ clippy::borrow_deref_ref,
+ clippy::semicolon_if_nothing_returned,
+ clippy::too_many_lines,
+ )]
+ fn size(&self) -> usize {
+ match self {
+ Self::Specific { year, month, day } => {
+ ::stef::buf::size_id(1)
+ + ::stef::buf::size_field(1, || { ::stef::buf::size_u16(*year) })
+ + ::stef::buf::size_field(2, || { (*month).size() })
+ + ::stef::buf::size_field(3, || { ::stef::buf::size_u8(*day) })
+ + ::stef::buf::size_u32(::stef::buf::END_MARKER)
+ }
+ Self::Secret { reason } => {
+ ::stef::buf::size_id(2)
+ + ::stef::buf::size_field_option(1, reason.as_ref())
+ + ::stef::buf::size_u32(::stef::buf::END_MARKER)
+ }
+ Self::Unknown => ::stef::buf::size_id(3),
+ }
+ }
+ }
+ /// Let's assume we only have details of people born **after** this year.
+ #[allow(dead_code)]
+ pub const MIN_YEAR: u16 = 1900;
+ /// Absolute maximum for a day, but might be even less depending
+ /// on the month.
+ #[allow(dead_code)]
+ pub const MAX_DAY: u8 = 31;
+ /// The month of the year.
+ #[derive(Clone, Debug, PartialEq)]
+ #[allow(clippy::module_name_repetitions, clippy::option_option)]
+ pub enum Month {
+ January,
+ February,
+ March,
+ April,
+ May,
+ June,
+ July,
+ August,
+ September,
+ October,
+ November,
+ December,
+ }
+ #[automatically_derived]
+ impl ::stef::Encode for Month {
+ #[allow(
+ clippy::borrow_deref_ref,
+ clippy::semicolon_if_nothing_returned,
+ clippy::too_many_lines,
+ )]
+ fn encode(&self, w: &mut impl ::stef::BufMut) {
+ match self {
+ Self::January => {
+ ::stef::buf::encode_id(w, 1);
+ }
+ Self::February => {
+ ::stef::buf::encode_id(w, 2);
+ }
+ Self::March => {
+ ::stef::buf::encode_id(w, 3);
+ }
+ Self::April => {
+ ::stef::buf::encode_id(w, 4);
+ }
+ Self::May => {
+ ::stef::buf::encode_id(w, 5);
+ }
+ Self::June => {
+ ::stef::buf::encode_id(w, 6);
+ }
+ Self::July => {
+ ::stef::buf::encode_id(w, 7);
+ }
+ Self::August => {
+ ::stef::buf::encode_id(w, 8);
+ }
+ Self::September => {
+ ::stef::buf::encode_id(w, 9);
+ }
+ Self::October => {
+ ::stef::buf::encode_id(w, 10);
+ }
+ Self::November => {
+ ::stef::buf::encode_id(w, 11);
+ }
+ Self::December => {
+ ::stef::buf::encode_id(w, 12);
+ }
+ }
+ }
+ }
+ #[automatically_derived]
+ impl ::stef::Decode for Month {
+ #[allow(clippy::too_many_lines)]
+ fn decode(r: &mut impl ::stef::Buf) -> ::stef::buf::Result {
+ match ::stef::buf::decode_id(r)? {
+ 1 => Ok(Self::January),
+ 2 => Ok(Self::February),
+ 3 => Ok(Self::March),
+ 4 => Ok(Self::April),
+ 5 => Ok(Self::May),
+ 6 => Ok(Self::June),
+ 7 => Ok(Self::July),
+ 8 => Ok(Self::August),
+ 9 => Ok(Self::September),
+ 10 => Ok(Self::October),
+ 11 => Ok(Self::November),
+ 12 => Ok(Self::December),
+ id => Err(::stef::buf::Error::UnknownVariant(id)),
+ }
+ }
+ }
+ #[automatically_derived]
+ impl ::stef::buf::Size for Month {
+ #[allow(
+ clippy::borrow_deref_ref,
+ clippy::semicolon_if_nothing_returned,
+ clippy::too_many_lines,
+ )]
+ fn size(&self) -> usize {
+ match self {
+ Self::January => ::stef::buf::size_id(1),
+ Self::February => ::stef::buf::size_id(2),
+ Self::March => ::stef::buf::size_id(3),
+ Self::April => ::stef::buf::size_id(4),
+ Self::May => ::stef::buf::size_id(5),
+ Self::June => ::stef::buf::size_id(6),
+ Self::July => ::stef::buf::size_id(7),
+ Self::August => ::stef::buf::size_id(8),
+ Self::September => ::stef::buf::size_id(9),
+ Self::October => ::stef::buf::size_id(10),
+ Self::November => ::stef::buf::size_id(11),
+ Self::December => ::stef::buf::size_id(12),
+ }
+ }
+ }
+}
+
diff --git a/crates/stef-doc/.gitignore b/crates/stef-doc/.gitignore
new file mode 100644
index 0000000..8c4b504
--- /dev/null
+++ b/crates/stef-doc/.gitignore
@@ -0,0 +1,2 @@
+/assets/style.css
+/node_modules
diff --git a/crates/stef-doc/Cargo.toml b/crates/stef-doc/Cargo.toml
new file mode 100644
index 0000000..a8fd25f
--- /dev/null
+++ b/crates/stef-doc/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "stef-doc"
+version.workspace = true
+authors.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+readme.workspace = true
+homepage.workspace = true
+repository.workspace = true
+license.workspace = true
+
+[dependencies]
+anyhow = "1.0.75"
+askama = { version = "0.12.1", default-features = false, features = ["markdown"] }
+heck = "0.4.1"
+stef-meta = { path = "../stef-meta" }
+stef-parser = { path = "../stef-parser" }
+
+[dev-dependencies]
+glob.workspace = true
+insta.workspace = true
+
+[lints]
+workspace = true
diff --git a/crates/stef-doc/examples/render.rs b/crates/stef-doc/examples/render.rs
new file mode 100644
index 0000000..90310e3
--- /dev/null
+++ b/crates/stef-doc/examples/render.rs
@@ -0,0 +1,48 @@
+//! Helper that renders all the test snapshot inputs as HTML files out into the OS's temp folder.
+//!
+//! All required assets are copied to the right location as well, so each input file is turned into
+//! an atomic output folder, as if it would be run through the CLI in the future (similar to
+//! `rustdoc` being invoked by `cargo doc`).
+
+use std::{env, fs, path::Path};
+
+use stef_doc::{Opts, Output};
+use stef_parser::Schema;
+
+fn write_output(output: &Output<'_>, parent: &Path) {
+ let path = parent.join(output.name);
+ let file = parent.join(&output.file);
+
+ fs::create_dir_all(file.parent().unwrap()).unwrap();
+ fs::write(file, &output.content).unwrap();
+
+ for module in &output.modules {
+ write_output(module, &path);
+ }
+}
+
+fn main() {
+ let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/inputs");
+ let out = env::temp_dir().join(concat!("stef/", env!("CARGO_PKG_NAME")));
+
+ for path in glob::glob(&format!("{dir}/*.stef")).unwrap() {
+ let path = path.unwrap();
+ let name = path.strip_prefix(dir).unwrap();
+
+ let input = fs::read_to_string(&path).unwrap();
+ let value = Schema::parse(input.as_str(), Some(name)).unwrap();
+ let value = stef_doc::render_schema(&Opts {}, &value).unwrap();
+
+ let out = out.join(name).with_extension("");
+
+ fs::remove_dir_all(&out).ok();
+ fs::create_dir_all(out.join("assets")).unwrap();
+ fs::copy(
+ concat!(env!("CARGO_MANIFEST_DIR"), "/assets/style.css"),
+ out.join("assets/style.css"),
+ )
+ .unwrap();
+
+ write_output(&value, &out);
+ }
+}
diff --git a/crates/stef-doc/input.css b/crates/stef-doc/input.css
new file mode 100644
index 0000000..c936f9a
--- /dev/null
+++ b/crates/stef-doc/input.css
@@ -0,0 +1,91 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ body {
+ @apply bg-main-200 text-main-800 dark:bg-main-800 dark:text-main-200;
+ }
+
+ main {
+ @apply container max-w-6xl px-12 py-4;
+ }
+
+ a {
+ @apply hover:underline;
+ }
+
+ table {
+ @apply table-auto border-collapse;
+ }
+
+ h1 {
+ @apply text-3xl font-bold;
+ }
+
+ h2 {
+ @apply text-2xl font-bold;
+ }
+
+ h3 {
+ @apply text-xl font-bold;
+ }
+
+ h4 {
+ @apply text-lg font-bold;
+ }
+}
+
+@layer components {
+ .markdown {
+ @apply markdown-neutral dark:markdown-invert max-w-none;
+ }
+
+ .name-module {
+ @apply text-black dark:text-white;
+ }
+
+ .name-struct {
+ @apply text-sky-600 dark:text-sky-400;
+ }
+
+ .name-enum {
+ @apply text-sky-600 dark:text-sky-400;
+ }
+
+ .name-variant {
+ @apply text-teal-600 dark:text-teal-400;
+ }
+
+ .name-alias {
+ @apply text-lime-600 dark:text-lime-400;
+ }
+
+ .name-const {
+ @apply text-orange-600 dark:text-orange-400;
+ }
+
+ .field-definition {
+ @apply mt-2 p-2 bg-main-300/50 dark:bg-main-700/50 rounded-sm;
+ }
+
+ .field-name {
+ @apply text-red-600 dark:text-red-400;
+ }
+
+ .field-type {
+ @apply text-purple-600 dark:text-purple-400;
+ }
+
+ .field-id {
+ @apply text-amber-600 dark:text-amber-400;
+ }
+
+ .section {
+ @apply rounded-sm bg-main-100/50 dark:bg-main-900/50 p-2 mt-2;
+ }
+
+ .item-definition {
+ @apply p-2 bg-main-300/50 dark:bg-main-700/50 rounded-sm overflow-x-auto;
+ }
+}
diff --git a/crates/stef-doc/package.json b/crates/stef-doc/package.json
new file mode 100644
index 0000000..4f7dbd5
--- /dev/null
+++ b/crates/stef-doc/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "stef-doc",
+ "private": true,
+ "scripts": {
+ "tailwind": "tailwindcss -i ./input.css -o ./assets/style.css",
+ "watch": "bun run tailwind --watch",
+ "build": "bun run tailwind --minify"
+ },
+ "dependencies": {
+ "@tailwindcss/typography": "^0.5.10",
+ "tailwindcss": "^3.4.0"
+ }
+}
diff --git a/crates/stef-doc/src/lib.rs b/crates/stef-doc/src/lib.rs
new file mode 100644
index 0000000..4ba4d74
--- /dev/null
+++ b/crates/stef-doc/src/lib.rs
@@ -0,0 +1,140 @@
+//! Documentation generator for Stef schema files, akin to Rust's `rustdoc`.
+
+use std::rc::Rc;
+
+use anyhow::Result;
+use askama::Template;
+use stef_parser::{Const, Definition, Enum, Module, Schema, Struct, TypeAlias};
+
+mod templates;
+
+/// Additional options to modify the behavior of the documentation generation.
+pub struct Opts {}
+
+/// The in-memory output of rendering a single [`Schema`] into HTML documentation.
+#[derive(Debug)]
+pub struct Output<'a> {
+ /// Name of the element, without any module path attached to it. For example, the plain struct
+ /// or module name.
+ pub name: &'a str,
+ /// Module path from the root of the schema to the current element/file.
+ pub path: Rc<[Rc]>,
+ /// Name of the output file. This might contain a parent directly name as well, in case it is a
+ /// module as each module is grouped into its own subfolder together with all its definitions.
+ pub file: String,
+ /// Fully generated content of the file that should be written to disk.
+ pub content: String,
+ /// Sub-modules of the root of the schema or currently represented module within.
+ pub modules: Vec