Skip to main content

Crate frontmatter_gen

Crate frontmatter_gen 

Source
Expand description

frontmatter-gen logo

frontmatter-gen

Front matter in, typed values out. YAML, TOML and JSON, with zero unsafe code.

Build status crates.io version API docs Coverage OpenSSF Scorecard OpenSSF Best Practices License MSRV 1.89.0

frontmatter-gen terminal demo: inspecting, validating, and converting Markdown front matter across YAML, JSON, and TOML


§Contents

Getting started

Library reference

Operational


§Install

[dependencies]
frontmatter-gen = "0.0.15"

Or from the command line:

cargo add frontmatter-gen

§Cargo features

All optional. The default build is the library alone, and its dependency set is deliberately small.

FeaturePulls inAdds
(default, none)—extraction, the three parsers, Value and Frontmatter
cliclapthe fmg binary and its argument parsing
ssgcli + tera, pulldown-cmark, dtt, urlthe static-site surface: Engine, Config, filesystem helpers
logging—log output from the library paths
cargo install frontmatter-gen --features cli   # the fmg binary

§Build from source

git clone https://github.com/sebastienrousseau/frontmatter-gen.git
cd frontmatter-gen
make          # check + clippy + test

§Requirements

  • Rust 1.89.0 or newer. rust-version in Cargo.toml is the floor and Cargo enforces it; CI builds on stable across Linux, macOS and Windows. See the minimum-toolchain policy.
  • A std platform. The crate uses std unconditionally; there is no no_std build.
  • No async runtime is required for the library. The ssg feature’s engine is async and uses Tokio.

§Quick Start

use frontmatter_gen::extract;

let content = "\
---
title: My Post
date: 2024-01-15
tags: [rust, parsing]
---
Hello World.

This is the body.";

let (frontmatter, body) = extract(content).expect("valid front matter");

assert_eq!(
    frontmatter.get("title").and_then(|v| v.as_str()),
    Some("My Post")
);
assert_eq!(
    frontmatter.get("tags").and_then(|v| v.array_len()),
    Some(2)
);
assert!(body.starts_with("Hello World."));

§What it does

frontmatter-gen finds the structured block at the top of a content file, tells you what format it is, and turns it into values you can index or deserialise.

Three shapes are recognised:

FormatDelimitersOpening line
YAML--- … ---title: Hello
TOML+++ … +++title = "Hello"
JSONa { … } object at the top of the file{"title": "Hello"}

Detection is automatic and the body is returned as a slice of the input, never a rebuilt string.


§Two halves, one pass

Finding the block and understanding it are separate operations, and both are public.

extractorparser
Answerswhere is the front matter, and what formatwhat does it contain
Returnsthe raw block and the body, as borrowsa Frontmatter of Values
Costsa scana parse

A caller that only needs the format, or only needs the body, does not pay for a parse. ADR-0002 records why.


§Library Usage

§Extract front matter and body

use frontmatter_gen::extract;

let (frontmatter, body) =
    extract("+++\ntitle = \"T\"\n+++\nBody").expect("valid front matter");

assert_eq!(frontmatter.get("title").and_then(|v| v.as_str()), Some("T"));
assert_eq!(body.trim(), "Body");

§Detect the format without parsing

use frontmatter_gen::extractor::{detect_format, extract_raw_frontmatter};
use frontmatter_gen::Format;

let (raw, body) =
    extract_raw_frontmatter("---\ntitle: T\n---\nBody").expect("a fence");
assert_eq!(detect_format(raw).expect("a known format"), Format::Yaml);
assert_eq!(body.trim(), "Body");

§Parse and serialise a known format

use frontmatter_gen::{parser::{parse, to_string}, Format, Value};

let frontmatter =
    parse("title: T\ncount: 2\n", Format::Yaml).expect("valid YAML");
assert_eq!(frontmatter.get("count"), Some(&Value::Number(2.0)));

// Round-trips through any of the three formats.
let json = to_string(&frontmatter, Format::Json).expect("serialise");
let back = parse(&json, Format::Json).expect("re-parse");
assert_eq!(back.get("title"), frontmatter.get("title"));

§Work with values

use frontmatter_gen::{Frontmatter, Value};

let mut frontmatter = Frontmatter::new();
let _ = frontmatter.insert("title".to_string(), Value::from("Hello"));
let _ = frontmatter.insert("draft".to_string(), Value::from(false));

assert!(frontmatter.contains_key("title"));
assert_eq!(frontmatter.get("draft"), Some(&Value::Boolean(false)));
assert_eq!(frontmatter.len(), 2);

Value is null, string, number, boolean, array, object, or a Tagged pair for YAML tags. Each variant has an is_* predicate, an as_* borrow and an into_* conversion that reports what it found instead.


§Configuration

parse applies no limits. parse_with_options does, when you ask it to:

use frontmatter_gen::{parser::{parse_with_options, ParseOptions}, Format};

let options = ParseOptions {
    max_depth: 8,
    max_keys: 256,
    validate: true,
};

let deep = r#"{"a": {"b": {"c": 1}}}"#;
assert!(parse_with_options(deep, Format::Json, Some(options)).is_ok());
OptionDefaultEffect when validate is true
max_depthsee ParseOptions::defaultrejects nesting deeper than this
max_keyssee ParseOptions::defaultrejects documents with more keys
validatefalsewhether either limit is applied at all

Validation is opt-in on purpose: the common caller reads files its own author wrote, where a spurious rejection is worse than the problem. Untrusted input should set validate: true. See ADR-0004 and Security.


§The CLI

Behind the cli feature, fmg extracts and validates from the shell:

cargo install frontmatter-gen --features cli

fmg extract post.md --format yaml
fmg validate post.md --required title,date

§Static site generation

Behind the ssg feature, Engine walks a content directory, splits front matter from body, renders through Tera and writes output; Config validates the site’s settings before anything runs. Every path it touches passes through the checks described under Security.

This surface is optional and off by default; the library does not pull Tera, a Markdown parser or a date library unless you ask for it.


§Examples

Run any of these with cargo run --example <name>:

ExampleShows
libthe high-level extract flow
extractorformat detection and raw extraction
parserparsing and serialising each format
typesValue and Frontmatter in use
fencedthe three delimiter styles
frontmatterbuilding front matter programmatically
errorevery error variant and how to recover

The target names come from Cargo.toml, which drops the _examples suffix the files carry.

make examples runs all of them; CI does the same on every push, so an example that stops working fails the build.


§When not to use frontmatter-gen

Cases where something else fits better, listed because the honest answer is “not yet” or “by design”.

  • You need no_std. The crate uses std unconditionally.
  • You need typed deserialisation straight into your struct. Frontmatter is a map of Value. Parse with serde directly against the raw block from extract_raw_frontmatter if you want a struct.
  • You need Display output to be valid JSON. It is a rendering, not a serialiser: escape_str escapes " and \ only, so a control character in a string produces text that will not re-parse. Use to_string(&fm, Format::Json).
  • You need to preserve comments, key order or quoting. Parsing is lossy in all three; there is no round-trip-faithful editor here.
  • You want a full static-site generator. The ssg feature is a useful core, not a finished product.

If you hit a case that should be on this list, please open an issue — that is how it gets fixed or moved into the supported set.


§Development

make              # check + clippy + test
make test         # all tests, all features
make clippy       # lints, warnings denied
make fmt          # formatting check
make lint         # markdownlint + codespell + REUSE
make doc          # rustdoc with warnings denied
make coverage     # line coverage gate (100% across all modules)
make miri         # lib tests under Miri
make fuzz         # build every target, replay corpus and regressions
make examples     # run every example
make bench-smoke  # compile and run each bench once
make versions     # every version-bearing file agrees
make demo         # regenerate .github/demo.gif via VHS
make deny / vet / audit   # supply chain

DEVELOPMENT.md maps each CI job to its local equivalent and explains the gotchas.

§Fuzzing

Three cargo-fuzz targets live under fuzz/fuzz_targets/:

cargo +nightly fuzz run fuzz_extract     # the body must be a suffix of the input
cargo +nightly fuzz run fuzz_parse       # every parser against every input
cargo +nightly fuzz run fuzz_roundtrip   # parse, serialise, re-parse, same keys

fuzz_roundtrip is the one that earns its keep: a serialiser that drops a key still produces output that parses, so nothing but a round trip notices. fuzz/corpus/<target> holds the committed seeds and fuzz/regressions/<target> every fixed-bug input; both replay on each push.

cargo-fuzz must be installed from source (cargo install --locked cargo-fuzz): the prebuilt binary is a musl build and infers its own build triple as the fuzz target.

§CI

WorkflowTriggerPurpose
ci.ymlpush, PRclippy, fmt, tests across three OSes, coverage, audit
quality.ymlpush, PRcoverage gate, Miri, fuzz replay, docs lint, cargo-vet ratchet, release hygiene

See CONTRIBUTING.md for signed commits and PR guidelines.


§Security

Reporting: never open a public issue for a vulnerability. See SECURITY.md for the private channel and disclosure policy.

§Input limits

ParseOptions carries max_depth, max_keys and max_size, and parse_with_options enforces them only when validate is true. A caller handling untrusted input must opt in. That is said plainly here because a limit nobody enables is not a limit.

§Path safety

Everything the ssg feature touches goes through utils::fs::validate_path_safety, which rejects backslashes, null bytes and control characters, .. traversal, symlinks, and Windows reserved device names. It deliberately does not decide which directory a caller may write to: only the caller knows its own root.

A defect fixed in v0.0.11 is worth naming: an absolute path used to return early from that function, skipping the symlink and reserved-name checks below it, so a symlink passed validation whenever it arrived as an absolute path. Every rule now applies to every path.

§Architectural posture

  • #![forbid(unsafe_code)] — the compiler proves the absence of unsafe blocks.
  • No C dependencies, no FFI, no network I/O.
  • Test-only crates live in [dev-dependencies], so a consumer’s build does not pull them.

§Supply chain

  • cargo-deny and cargo-audit in CI.
  • cargo-vet provenance in supply-chain/, with an exemption baseline the CI ratchet cannot exceed.
  • noyalib and dtt pinned exactly (=0.0.X); a bump is a deliberate release (ADR-0005).
  • Cargo.lock committed; CI builds --locked. Actions pinned by SHA.
  • REUSE 3.3 compliant, linted in CI.
  • Commits on main are signed; releases are signed tags (KEYS.asc).

§Documentation

The four entry points, identical across every repo in the family:

DocumentCovers
CHANGELOG.mdper-release notes, Keep a Changelog format
SECURITY.mddisclosure policy, limits, path safety, supply chain
CONTRIBUTING.mdbranch and commit conventions, PR expectations
GOVERNANCE.mdwho decides what, how changes land
SUPPORT.mdwhere to ask, what to expect
AGENTS.mdinvariants for AI-assisted contributions

§Stability guarantees

  • Versioning. SemVer, with the pre-1.0 posture that the patch number is the breaking axis during 0.0.x. Releases increment by +0.0.1 and every breaking change is called out in CHANGELOG.md.
  • Output stability. What the crate produces is part of the API: a change to how a document parses, which Value variant a scalar becomes, or what to_string emits is treated as breaking even when no Rust signature moves.
  • Deprecations live for at least two releases with a #[deprecated] note naming the replacement before removal.
  • Version-bearing files are checked against the manifest by scripts/verify-release-versions.sh before a tag exists, so an install snippet cannot go stale. That gate is why this README’s version is right.

§Minimum-toolchain policy

The floor is Rust 1.89.0, declared as rust-version in Cargo.toml so Cargo refuses older toolchains with a clear message.

  • When it may rise: only on a release, never silently, and always with the reason in the changelog entry.
  • Why it is where it is: 1.89.0 is the floor shared by the maintainer’s Rust crates. The dependency graph alone needs 1.88.0.
  • What is verified: CI builds and tests on stable. The floor is the version Cargo enforces from the manifest.

No claim is made about distro-LTS toolchains. Making one would require a table mapping current distro versions to this floor, and an aspirational claim there is worse than none.


§License

Dual-licensed under Apache 2.0 or MIT, at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you shall be dual-licensed as above, without any additional terms or conditions.

Back to top

# Frontmatter Gen

frontmatter-gen is a fast, secure, and memory-efficient library for working with frontmatter in multiple formats (YAML, TOML, and JSON).

§Overview

This library provides robust handling of frontmatter with the following key features:

  • Zero-copy parsing for optimal memory efficiency
  • Type-safe operations with comprehensive error handling
  • Multiple format support (YAML, TOML, JSON)
  • Secure processing with input validation and size limits
  • Async support with the ssg feature flag

§Security Features

  • Input validation to prevent malicious content
  • Size limits to prevent denial of service attacks
  • Safe string handling to prevent memory corruption
  • Secure path handling for file operations

§Quick Start

use frontmatter_gen::{extract, Format, Frontmatter, Result};

let content = r#"---
title: Test Post
date: 2025-09-09
---
Content here"#;

let result = extract(content);
assert!(result.is_ok());
let (frontmatter, content) = result.unwrap();
assert_eq!(
    frontmatter.get("title").and_then(|v| v.as_str()),
    Some("Test Post")
);
assert_eq!(content.trim(), "Content here");

§Feature Flags

  • default: Core frontmatter functionality
  • cli: Command-line interface support
  • ssg: Static Site Generator functionality (includes CLI)

§Error Handling

All operations return a Result type with detailed error information. Use frontmatter.get("key") to access fields and pattern-match on Error variants for granular error handling.

Re-exports§

pub use crate::config::Config;
pub use crate::error::Error;
pub use crate::extractor::detect_format;
pub use crate::extractor::extract_raw_frontmatter;
pub use crate::parser::parse;
pub use crate::parser::to_string;
pub use crate::types::Format;
pub use crate::types::Frontmatter;
pub use crate::types::Value;

Modules§

cli
Command Line Interface Module
config
Configuration Module
engine
Site Generation Engine
error
Error handling for the frontmatter-gen crate.
extractor
This module provides functionality for extracting frontmatter from content.
parser
Front Matter Parser and Serialiser Module
prelude
Prelude module for convenient imports.
ssg
Static Site Generator Module
types
This module defines the core types used throughout the frontmatter-gen crate. It includes the Format enum for representing different frontmatter formats, the Value enum for representing various data types that can be stored in frontmatter, and the Frontmatter struct which is the main container for frontmatter data.
utils
Utility Module

Structs§

ParseOptions
Configuration options for parsing operations.

Constants§

MAX_FRONTMATTER_SIZE
Maximum size allowed for frontmatter content (1 MiB).
MAX_NESTING_DEPTH
Maximum allowed nesting depth for structured data (32 levels).

Functions§

extract
Extracts and parses frontmatter from content with format auto-detection.
to_format
Converts frontmatter to a specific format.
validate_input
Validates input content against security constraints.

Type Aliases§

Result
A specialized Result type for frontmatter operations.