outlint-core 0.1.0

Markdown outline schema validator (core library)
Documentation

outlint-core

Validate the header structure (outline) of Markdown documents against a declarative schema.

This is the pure, IO-free core: it turns source text into normalized values and diagnostics. It never touches the filesystem, the network, the terminal, or the process exit status — that shell is the outlint CLI crate. Callers supply the schema text, the Markdown text, and any linked JSON Schema resources they read themselves.

This is a 0.x release: expect breaking changes to the API, the schema language, and the diagnostic set before 1.0. The normative specification is spec/outlint-spec.md; where the two disagree, the specification wins.

What it checks

  • Outline structure — the section tree built from ATX and Setext headings, including skipped heading levels.
  • Section rules — first-match-wins matchers (exact, glob, regex, *), allow: false denials, and strict scopes that reject unmatched headings.
  • Cardinalityrequired and repeat: "min..max" per rule, per scope.
  • Cross-section logicone_of, any_of, at_most_one, all_or_none, requires, conflicts, and ordered constraints over rules addressed by id.
  • YAML frontmatter — presence policy and delegated validation of the frontmatter mapping against an inline or linked JSON Schema (draft 2020-12). Inline schemas are self-contained and permit fragment-only references; linked schemas may span local files. Frontmatter also supports fm. propositions in constraints: fm.key presence and fm.key=value typed scalar equality under the YAML core schema, with dotted paths into nested mappings.

Diagnostics carry stable ids (missing-section, unexpected-section, ordered, frontmatter-schema, …), document source anchors, and structural schema-node addresses. Resolve a diagnostic's schema_node through loaded.locations.nodes, then use the resulting SourceRange::source to find the source text and label in loaded.sources.documents.

Usage

use outlint_core::{
    load_schema, parse_markdown, DiagnosticTarget, MarkdownOptions, PreparedValidator,
};

fn main() {
    // 1. Load a schema from YAML (or JSON) source text.
    let loaded = load_schema(
        r#"
version: 1
title: "*"
sections:
  - match: "Overview"
    required: true
"#,
    )
    .expect("schema is valid");

    // 2. Compile it once; reuse for any number of documents.
    let validator = PreparedValidator::new(&loaded.schema).expect("schema compiles");

    // 3. Parse Markdown into a section tree.
    let document = parse_markdown(
        "# Widget Redesign\n\n## Usage\n",
        MarkdownOptions {
            strip_inline_markup: loaded.schema.options.strip_inline_markup,
        },
    );

    // 4. Inspect the diagnostics. The target distinguishes a heading that is
    //    really there from a schema matcher that nothing matched.
    for diagnostic in validator.validate(&document) {
        let target = match &diagnostic.target {
            DiagnosticTarget::Header(path) => path.to_string(),
            DiagnosticTarget::MissingHeader { matcher, .. } => format!("expected {matcher}"),
            DiagnosticTarget::Document => "document".to_owned(),
            DiagnosticTarget::Frontmatter { .. } => "frontmatter".to_owned(),
        };
        println!(
            "{}:{} [{}] {} ({target})",
            diagnostic.location.line,
            diagnostic.location.column,
            diagnostic.id.as_str(),
            diagnostic.message,
        );
    }
}

Output:

1:1 [missing-section] matched 0 sections, but at least 1 are required (expected Overview)

load_schema returns Result<LoadedSchema, InvalidSchema>; InvalidSchema carries every schema error together with the source text needed to render it:

use outlint_core::load_schema;

if let Err(invalid) = load_schema("version: 99\n") {
    for error in invalid.errors.iter() {
        let source = &invalid.sources.documents[&error.range.source];
        eprintln!(
            "{} at bytes {}..{} in {}",
            error.kind.as_str(),
            error.range.range.start.0,
            error.range.range.end.0,
            source.label.as_ref().map_or("<schema>", |label| &label.0),
        );
    }
}

For schemas whose frontmatter.schema points at an external JSON Schema file, the caller owns the IO boundary. Use linked_frontmatter_schema_path to find the root path, assign that file an absolute logical URI, then walk its local reference graph with json_schema_external_references. The helper returns both the lexical physical_uri to read and the $id-aware logical_uri under which to register the contents. Ignore same-document references, deduplicate or cycle-check reads, record read failures rather than dropping them, and place every attempted resource in a LinkedJsonSchemaInput passed to load_schema_with_resources. Core never retrieves remote references.

Related

MSRV

Rust 1.86.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option (MIT OR Apache-2.0).