GitHub

Numbered-indent markup

A small numbered-indent markup language and an Odin parser for it.

Trees without significant whitespace: depth is a number at the start of the line. Requires Odin.

0 person
1 name: Ada
1 address
2 city: London
2 note: see section 3: extras
01

Format

See examples/ok.bnml for a valid file and examples/bad.bnml for an indent jump that fails validation.

examples/ok.bnml:

; A valid BNML tree.
; Blank lines and comments are ignored.

0 person
1 name: Ada
1 address
2 city: London
2 note: see section 3: extras

examples/bad.bnml:

; Invalid: depth jumps from 0 to 2.
0 person
2 city: London
02

Build

The package is bnml at the repository root.

odin build cmd/bnml -out:bnml

C bindings. Build on the machine you will run on (Odin does not cross-link Windows or macOS from Linux). From the repository root:

sh c/build.sh          # Linux, macOS, Git Bash
c\build.bat            # Windows cmd

That produces the shared lib, a static lib, bnml_c / bnml_c.exe, and the C tests (bnml_c_test; Unix also builds bnml_c_test_static). The scripts run the tests. Run them from the repository root so examples/ok.bnml resolves.

Manual commands:

# Linux
odin build c -build-mode:shared -no-entry-point -out:libbnml.so \
  -extra-linker-flags:"-Wl,--version-script=c/libbnml.version,-soname,libbnml.so"
cc -I c examples/c/main.c ./libbnml.so -Wl,-rpath,'$ORIGIN' -o bnml_c
odin build c -build-mode:static -no-entry-point -out:libbnml.a
cc -I c examples/c/main.c libbnml.a -lpthread -ldl -lm -o bnml_c

# macOS (lld: Apple ld mishandles Odin's quoted -init)
odin build c -build-mode:shared -no-entry-point -linker:lld -out:libbnml.dylib \
  -extra-linker-flags:"-Wl,-exported_symbols_list,c/libbnml.exports,-install_name,@rpath/libbnml.dylib"
cc -I c examples/c/main.c ./libbnml.dylib -Wl,-rpath,@loader_path -o bnml_c
odin build c -build-mode:static -no-entry-point -out:libbnml.a
cc -I c examples/c/main.c libbnml.a -o bnml_c

# Windows (native). Define BNML_DLL when using the DLL.
odin build c -build-mode:shared -out:bnml.dll
cl /I c /DBNML_DLL examples\c\main.c /Fe:bnml_c.exe bnml.lib
# or: gcc -I c -DBNML_DLL examples/c/main.c bnml.dll -o bnml_c.exe

Prefer the shared library on Windows. A static .lib/.a still needs the Odin runtime’s system libraries (kernel32, ws2_32, winmm, …); the set depends on the toolchain, so linking a Windows static lib is not covered here.

Shared builds hide non-API symbols: GNU ld uses c/libbnml.version, Darwin uses c/libbnml.exports — keep those two in sync. -no-entry-point is for static libs and Unix shared objects so they do not define main. Windows DLLs keep an empty main because DllMain calls it after starting the Odin runtime.

macOS shared builds use -linker:lld. Odin 2026-09 passes -Wl,-init,'__odin_entry_point'; Apple’s ld treats those quotes as part of the symbol name. lld ignores -init, which is fine — the C API starts the Odin runtime on the first call. Expect ld64.lld: warning: Option '-init' is not yet implemented.

03

Tests

From the repository root:

odin test tests

C tests run as part of c/build.sh / c/build.bat. Run those from the repository root so examples/ok.bnml resolves.

04

CLI

Three commands. Each takes a file path.

bnml validate <path>
bnml parse <path>
bnml find <path> <key>
./bnml validate examples/ok.bnml
./bnml parse examples/ok.bnml
./bnml find examples/ok.bnml name
./bnml validate examples/bad.bnml
Command What it does
validate Exit 0 if the file is valid (prints a success line).
parse Print the tree.
find Print each matching node and its descendants (exact key). Exit 1 if none.
05

Library

Node has key, value, and children. Errors are File_Read_Failed, Invalid_Indent_Sequence, Malformed_Line, and Out_Of_Memory.

read_lines and strip are separate from parse. Without strip, blank and comment lines are Malformed_Line.

package main

import bnml "path/to/bnml"
import "core:fmt"

main :: proc() {
	lines, data, err := bnml.read_lines("file.bnml")
	if err != .None {
		fmt.println(err)
		return
	}
	defer delete(data)
	defer delete(lines)

	bnml.strip(&lines)

	roots, parse_err := bnml.parse(lines[:])
	if parse_err != .None {
		fmt.println(parse_err)
		return
	}
	defer bnml.destroy_tree(&roots)

	bnml.print_tree(roots[:])

	hits, find_err := bnml.find_nodes(roots[:], "name")
	if find_err != .None {
		fmt.println(find_err)
		return
	}
	defer delete(hits)
}
Procedure What it does
read_lines Load a file into lines.
strip Trim lines; drop blanks and whole-line ; comments.
validate Check prefixes / indent sequence.
parse Validate + build the tree.
parse_unchecked Build the tree without validate. Lines with no digit prefix are treated as depth 0; indent jumps still fail.
find_nodes Collect every node with that exact key.
print_tree / print_node Dump a node and its descendants to stdout.
destroy_node Free one node and its children.
destroy_tree Free the tree.
06

C API

c/bnml.h is the public header. BnmlTree and BnmlNode are opaque. BnmlError is int32_t. The C API is not thread-safe.

Parse requires *out_tree == NULL; a live pointer returns BNML_OUTPUT_NOT_NULL and is left unchanged. Trees from parse own their nodes; free them with bnml_tree_destroy (NULL is a no-op). Accessor strings and find pointers are valid until that destroy.

For a non-NULL node, key and value are never NULL; a missing value is "". They are C strings, so an interior NUL in the document is not representable. bnml_find returns 0 if the tree or key is NULL.

A CLI-shaped example is examples/c/main.c.

#include "bnml.h"
#include <stdio.h>

int main(void) {
	BnmlTree *tree = NULL;
	BnmlError err = bnml_parse_file("file.bnml", &tree);
	if (err != BNML_OK) {
		fprintf(stderr, "%s\n", bnml_error_string(err));
		return 1;
	}

	bnml_print_tree(tree);

	BnmlNode *hits[8];
	size_t n = bnml_find(tree, "name", hits, 8);
	for (size_t i = 0; i < n && i < 8; i++) {
		printf("%s: %s\n", bnml_node_key(hits[i]), bnml_node_value(hits[i]));
	}

	bnml_tree_destroy(tree);
	return 0;
}

Parse and validate always strip blank lines and whole-line ; comments, same as the CLI.

Function What it does
bnml_parse_file / bnml_parse_string / bnml_parse_buffer Strip + parse into a tree.
bnml_validate_file / bnml_validate_string / bnml_validate_buffer Strip, then check prefixes / indent sequence.
bnml_tree_root_count / bnml_tree_root Access root nodes.
bnml_node_key / bnml_node_value / bnml_node_child_count / bnml_node_child Access a node.
bnml_find Count matches (may exceed cap); write up to cap pointers into the tree. out == NULL returns only the count.
bnml_print_tree / bnml_print_node Dump a node and its descendants to stdout.
bnml_tree_destroy Free the tree (NULL is a no-op).
bnml_error_string Message for an error code.

bnml_parse_buffer / bnml_validate_buffer take a pointer and length; they do not need a terminating NUL. text may be NULL only when len is 0. If len does not fit in a signed pointer-sized int, the result is BNML_ARGUMENT_TOO_LARGE.

Errors: BNML_FILE_READ_FAILED, BNML_INVALID_INDENT_SEQUENCE, BNML_MALFORMED_LINE, BNML_OUT_OF_MEMORY, BNML_NULL_ARGUMENT, BNML_OUTPUT_NOT_NULL, BNML_ARGUMENT_TOO_LARGE.

07

Status

v0 — the format may change.