header-parsing 0.3.2

Simplifies parsing the headers of markdown inspired file formats
Documentation

Header parsing

This library is meant to help with parsing markdown inspired files, where headers are marked by a sequence of "#".

The only function parse_header is meant to be used when parsing a file line by line. If the line starts with a "#", the function will return Some result to indicate if it's a valid format, else it will return None.

The only invalid format is when a header starts with more than one "#" more than the previous header.

This makes using markdown like formats viable for config files.

Example config

Headers and subheaders are stored as array.

For example you could have a file like this:

A

# Header 1

B

## Subheader 1

C

## Subheader 2

D

# Header 2

E

## Subheader 1

F

## Subheader 2

G

The letters would belong to different headers:

  • A belongs to no subheader ([])
  • B belongs to "Header 1" (["Header 1"])
  • C belongs to "Subheader 1" of "Header 1" (["Header 1", "Subheader 1"])
  • D belongs to "Subheader 2" of "Header 1" (["Header 1", "Subheader 2"])
  • E belongs to "Header 2" (["Header 2"])
  • F belongs to "Subheader 1" of "Header 2" (["Header 2", "Subheader 1"])
  • G belongs to "Subheader 2" of "Header 2" (["Header 2", "Subheader 2"])

Usage

You have to store the path of headers and subheader yourself. This way, you are allowed to handle the sections inbetween the headers as you want.

use header_parsing::parse_header;

use std::io::{BufRead, BufReader, Read};

enum Error {
    SubheaderWithoutHeader,
    ... // Custom error types
}

fn parse<R: Read>(reader: R) -> Result<Map<Vec<Box<str>>, Value>, Error> {
    // a `Vec<Box<str>>`
    let mut current_path = Vec::new();
    // the result is likely a map type, like a `HashMap`
    let mut result = Map::new();
    // the content inbetween the headers parsed into the wanted format
    let mut value = Value::new();

    for line in BufReader::new(reader).lines() {
        let Ok(line) = line else {
            return Err(...);
        };

        // if current line is a header
        if let Some(success) = parse_header(&mut current_path, &line) {
            if let Ok(path_changes) = success {
                // add parsed value to previous subheader
                result.insert(path_changes.path.clone(), value);
                // start parsing next subsection
                value = Value::new();
                // apply path changes
                path_changes.apply();
            } else {
                return Err(Error::SubheaderWithoutHeader);
            }
        } else {
            // parse the content inbetween headers
            value.modify(parse_line(&line)?);
        }
    }

    result.insert(current_path, value);

    Ok(result)
}

Simpler usage with Parser

If you don't need to access the path before applying the changes, Parser tracks the path for you and applies the changes automatically. Every line is classified as either a Header (with its nesting level, 0 for #) or Content:

use header_parsing::{Line, Parser};

let mut parser = Parser::new();

for line in content.lines() {
    match parser.line(line) {
        Ok(Line::Header { level }) => {
            // `parser.path()` now contains the full path of headers
        }
        Ok(Line::Content(line)) => {
            // the content inbetween headers
        }
        Err(_) => { /* subheader without a header */ }
    }
}