/* frontmatter.h — portable, header-only markdown frontmatter library. * * Parses a markdown document with an optional `---` fenced frontmatter block * into typed values. Zero dependencies: libc only. Not full YAML: simple * `key: value` lines. Values may be quoted (`"..."` or `'...'`). List fields * accept CSV (`tags: a, b, c`) or JSON arrays (`tags: ["a", "b"]`). * * Format: * - Frontmatter is delimited by a leading `---` line and a closing `---` * line. An unterminated fence (no closing `---`) means the whole text is * body — no metadata. * - Lines are `key: value`. Blank lines, `#` comments, and lines without a * colon are skipped. Keys and values are trimmed. * - Value types: `true`/`false` → bool; bare integer → int; `[...]` → list * (JSON array; non-string elements are serialized); anything else → * string (quotes stripped if present). * - The body is everything after the closing fence, trailing whitespace * trimmed. Documents without frontmatter: whole text, trailing * whitespace trimmed. * * Usage: #include "frontmatter.h" in your code. The header is complete on * its own; all functions are static inline, safe to include in any number * of translation units. * * API: * parse_frontmatter(text, &doc) split frontmatter + body * doc_get(&doc, "key") value or NULL * doc_free(&doc) free the document * fm_value_free(&val) free one value * fm_list_items(&val, &items) list-valued field as string items * fm_json_parse(text) embedded JSON value or NULL (invalid) * fm_json_free(v) / fm_json_get(obj, key) / fm_json_serialize(v) * fm_json_merge_owned(base, over) shallow object merge; owns both inputs * * Concurrency: no shared state. Each call is self-contained. */ #ifndef FRONTMATTER_H #define FRONTMATTER_H #include #include #include #include #include #include #include #include #ifdef __cplusplus extern "C" { #endif /* ------------------------------------------------------------------ */ /* Allocation */ /* ------------------------------------------------------------------ */ static inline void *fm_xmalloc(size_t n) { void *p = malloc(n ? n : 1); if (!p) { fprintf(stderr, "frontmatter: out of memory\n"); abort(); } return p; } static inline void *fm_xrealloc(void *p, size_t n) { void *q = realloc(p, n ? n : 1); if (!q) { fprintf(stderr, "frontmatter: out of memory\n"); abort(); } return q; } static inline char *fm_strdup(const char *s) { size_t n = s ? strlen(s) : 0; char *p = (char *)fm_xmalloc(n + 1); if (s) memcpy(p, s, n); p[n] = '\0'; return p; } static inline char *fm_strndup(const char *s, size_t n) { char *p = (char *)fm_xmalloc(n + 1); memcpy(p, s, n); p[n] = '\0'; return p; } static inline char *fm_vsnprintf(const char *fmt, va_list ap) { va_list ap2; va_copy(ap2, ap); int n = vsnprintf(NULL, 0, fmt, ap2); va_end(ap2); if (n < 0) return fm_strdup(""); char *p = (char *)fm_xmalloc((size_t)n + 1); vsnprintf(p, (size_t)n + 1, fmt, ap); return p; } static inline char *fm_strfmt(const char *fmt, ...) { va_list ap; va_start(ap, fmt); char *s = fm_vsnprintf(fmt, ap); va_end(ap); return s; } /* ------------------------------------------------------------------ */ /* Growable byte buffer (NUL-terminated on steal) */ /* ------------------------------------------------------------------ */ typedef struct { char *data; size_t len; size_t cap; } fm_buf; static inline void fm_buf_init(fm_buf *b) { b->data = NULL; b->len = 0; b->cap = 0; } static inline void fm_buf_free(fm_buf *b) { free(b->data); b->data = NULL; b->len = 0; b->cap = 0; } static inline void fm_buf_reset(fm_buf *b) { b->len = 0; } static inline void fm_buf_reserve(fm_buf *b, size_t extra) { if (b->len + extra + 1 > b->cap) { size_t cap = b->cap ? b->cap : 64; while (cap < b->len + extra + 1) cap *= 2; b->data = (char *)fm_xrealloc(b->data, cap); b->cap = cap; } } static inline void fm_buf_putn(fm_buf *b, const char *s, size_t n) { fm_buf_reserve(b, n); memcpy(b->data + b->len, s, n); b->len += n; b->data[b->len] = '\0'; } static inline void fm_buf_put(fm_buf *b, const char *s) { if (s) fm_buf_putn(b, s, strlen(s)); } static inline void fm_buf_push(fm_buf *b, char c) { fm_buf_reserve(b, 1); b->data[b->len++] = c; b->data[b->len] = '\0'; } static inline int fm_buf_fmt(fm_buf *b, const char *fmt, ...) { va_list ap, ap2; if (!fmt) return 0; va_start(ap, fmt); va_copy(ap2, ap); int n = vsnprintf(NULL, 0, fmt, ap2); va_end(ap2); if (n < 0) { va_end(ap); return -1; } fm_buf_reserve(b, (size_t)n); vsnprintf(b->data + b->len, (size_t)n + 1, fmt, ap); va_end(ap); b->len += (size_t)n; return n; } static inline char *fm_buf_steal(fm_buf *b) { if (!b->data) b->data = (char *)fm_xmalloc(1); b->data[b->len] = '\0'; char *s = b->data; b->data = NULL; b->len = 0; b->cap = 0; return s; } /* ------------------------------------------------------------------ */ /* String list */ /* ------------------------------------------------------------------ */ typedef struct { char **items; size_t len; size_t cap; } fm_strlist; static inline void fm_sl_init(fm_strlist *l) { l->items = NULL; l->len = 0; l->cap = 0; } static inline void fm_sl_free(fm_strlist *l) { for (size_t i = 0; i < l->len; i++) free(l->items[i]); free(l->items); fm_sl_init(l); } static inline void fm_sl_push(fm_strlist *l, char *owned) { if (l->len + 1 > l->cap) { l->cap = l->cap ? l->cap * 2 : 8; l->items = (char **)fm_xrealloc(l->items, l->cap * sizeof(char *)); } l->items[l->len++] = owned; } static inline void fm_sl_push_copy(fm_strlist *l, const char *s) { fm_sl_push(l, fm_strdup(s)); } static inline void fm_sl_pop(fm_strlist *l) { if (!l->len) return; free(l->items[--l->len]); } /* ------------------------------------------------------------------ */ /* Embedded JSON values */ /* Object, array, string, number, bool, null. Parse, serialize, */ /* lookup, shallow-merge. */ /* ------------------------------------------------------------------ */ enum { FMJ_OBJECT, FMJ_ARRAY, FMJ_STRING, FMJ_NUMBER, FMJ_BOOL, FMJ_NULL, }; typedef struct fm_json { int type; char *key; /* OBJECT member key (owned), else NULL */ char *str; /* FMJ_STRING value (owned), else NULL */ double num; /* FMJ_NUMBER */ int is_int; /* FMJ_NUMBER was an integer literal in range */ long long i; /* FMJ_NUMBER integral value */ int b; /* FMJ_BOOL */ struct fm_json **items; /* OBJECT/ARRAY members (owned) */ size_t count; } fm_json; static inline fm_json *fm_json_new(int type) { fm_json *v = (fm_json *)fm_xmalloc(sizeof *v); memset(v, 0, sizeof *v); v->type = type; return v; } static inline void fm_json_free(fm_json *v) { if (!v) return; for (size_t i = 0; i < v->count; i++) fm_json_free(v->items[i]); free(v->items); free(v->key); free(v->str); free(v); } static inline const fm_json *fm_json_get(const fm_json *o, const char *key) { if (!o || o->type != FMJ_OBJECT || !key) return NULL; for (size_t i = 0; i < o->count; i++) if (o->items[i] && o->items[i]->key && strcmp(o->items[i]->key, key) == 0) return o->items[i]; return NULL; } /* Max nesting depth for the recursive-descent JSON parser; bounds stack * growth so a pathologically deep input (e.g. 100k nested arrays) is * rejected with an error instead of overflowing the C stack. */ #define FM_JSON_MAX_DEPTH 256 typedef struct { const char *p; const char *end; int err; size_t depth; } fm_jp; static inline void fm_jskip(fm_jp *s) { while (s->p < s->end && (*s->p == ' ' || *s->p == '\t' || *s->p == '\n' || *s->p == '\r')) s->p++; } /* String at s->p (opening quote). Returns owned unescaped text. */ static inline char *fm_jstring_raw(fm_jp *s) { if (s->p >= s->end || *s->p != '"') { s->err = 1; return NULL; } s->p++; fm_buf b; fm_buf_init(&b); while (s->p < s->end && *s->p != '"') { char c = *s->p++; if (c == '\\' && s->p < s->end) { char e = *s->p++; switch (e) { case '"': fm_buf_push(&b, '"'); break; case '\\': fm_buf_push(&b, '\\'); break; case '/': fm_buf_push(&b, '/'); break; case 'b': fm_buf_push(&b, '\b'); break; case 'f': fm_buf_push(&b, '\f'); break; case 'n': fm_buf_push(&b, '\n'); break; case 'r': fm_buf_push(&b, '\r'); break; case 't': fm_buf_push(&b, '\t'); break; case 'u': { if (s->end - s->p < 4) goto fail; unsigned h = 0; bool ok = true; for (int k = 0; k < 4; k++) { char hc = s->p[k]; int d; if (hc >= '0' && hc <= '9') d = hc - '0'; else if (hc >= 'a' && hc <= 'f') d = hc - 'a' + 10; else if (hc >= 'A' && hc <= 'F') d = hc - 'A' + 10; else { ok = false; break; } h = h * 16 + (unsigned)d; } if (!ok) goto fail; s->p += 4; /* Join a UTF-16 surrogate pair (high \uD800-\uDBFF followed by * low \uDC00-\uDFFF) into a single codepoint so astral chars * (emoji, CJK ext-B, …) emit proper 4-byte UTF-8 rather than * two CESU-8 3-byte sequences. Lone surrogates fall through to * the 3-byte (lenient) encoding below. */ unsigned cp = h; if (h >= 0xD800u && h <= 0xDBFFu && s->end - s->p >= 6 && s->p[0] == '\\' && s->p[1] == 'u') { unsigned lo = 0; bool lok = true; for (int k = 0; k < 4; k++) { char hc = s->p[2 + k]; int d; if (hc >= '0' && hc <= '9') d = hc - '0'; else if (hc >= 'a' && hc <= 'f') d = hc - 'a' + 10; else if (hc >= 'A' && hc <= 'F') d = hc - 'A' + 10; else { lok = false; break; } lo = lo * 16 + (unsigned)d; } if (lok && lo >= 0xDC00u && lo <= 0xDFFFu) { cp = 0x10000u + ((h - 0xD800u) << 10) + (lo - 0xDC00u); s->p += 6; } } if (cp < 0x80) { fm_buf_push(&b, (char)cp); } else if (cp < 0x800) { fm_buf_push(&b, (char)(0xC0 | (cp >> 6))); fm_buf_push(&b, (char)(0x80 | (cp & 0x3F))); } else if (cp < 0x10000) { fm_buf_push(&b, (char)(0xE0 | (cp >> 12))); fm_buf_push(&b, (char)(0x80 | ((cp >> 6) & 0x3F))); fm_buf_push(&b, (char)(0x80 | (cp & 0x3F))); } else { fm_buf_push(&b, (char)(0xF0 | (cp >> 18))); fm_buf_push(&b, (char)(0x80 | ((cp >> 12) & 0x3F))); fm_buf_push(&b, (char)(0x80 | ((cp >> 6) & 0x3F))); fm_buf_push(&b, (char)(0x80 | (cp & 0x3F))); } break; } default: goto fail; } } else if (c < 0x20) { goto fail; /* raw control char: invalid JSON */ } else { fm_buf_push(&b, c); } } if (s->p >= s->end || *s->p != '"') goto fail; s->p++; return fm_buf_steal(&b); fail: s->err = 1; fm_buf_free(&b); return NULL; } static inline fm_json *fm_jvalue(fm_jp *s); static inline fm_json *fm_jobject(fm_jp *s) { if (s->depth >= FM_JSON_MAX_DEPTH) { s->err = 1; return NULL; } s->depth++; s->p++; /* { */ fm_json *o = fm_json_new(FMJ_OBJECT); for (;;) { fm_jskip(s); if (s->p < s->end && *s->p == '}') { s->p++; s->depth--; return o; } char *key = fm_jstring_raw(s); if (!key) { fm_json_free(o); return NULL; } fm_jskip(s); if (s->p >= s->end || *s->p != ':') { free(key); fm_json_free(o); s->err = 1; return NULL; } s->p++; fm_json *v = fm_jvalue(s); if (!v) { free(key); fm_json_free(o); return NULL; } v->key = key; o->items = (fm_json **)fm_xrealloc(o->items, (o->count + 1) * sizeof(fm_json *)); o->items[o->count++] = v; fm_jskip(s); if (s->p < s->end && *s->p == ',') { s->p++; continue; } if (s->p < s->end && *s->p == '}') { s->p++; s->depth--; return o; } fm_json_free(o); s->err = 1; return NULL; } } static inline fm_json *fm_jarray(fm_jp *s) { if (s->depth >= FM_JSON_MAX_DEPTH) { s->err = 1; return NULL; } s->depth++; s->p++; /* [ */ fm_json *a = fm_json_new(FMJ_ARRAY); for (;;) { fm_jskip(s); if (s->p < s->end && *s->p == ']') { s->p++; s->depth--; return a; } fm_json *v = fm_jvalue(s); if (!v) { fm_json_free(a); return NULL; } a->items = (fm_json **)fm_xrealloc(a->items, (a->count + 1) * sizeof(fm_json *)); a->items[a->count++] = v; fm_jskip(s); if (s->p < s->end && *s->p == ',') { s->p++; continue; } if (s->p < s->end && *s->p == ']') { s->p++; s->depth--; return a; } fm_json_free(a); s->err = 1; return NULL; } } static inline fm_json *fm_jvalue(fm_jp *s) { fm_jskip(s); if (s->p >= s->end) { s->err = 1; return NULL; } char c = *s->p; if (c == '{') return fm_jobject(s); if (c == '[') return fm_jarray(s); if (c == '"') { char *t = fm_jstring_raw(s); if (!t) return NULL; fm_json *v = fm_json_new(FMJ_STRING); v->str = t; return v; } if (s->end - s->p >= 4 && strncmp(s->p, "true", 4) == 0) { s->p += 4; fm_json *v = fm_json_new(FMJ_BOOL); v->b = 1; return v; } if (s->end - s->p >= 5 && strncmp(s->p, "false", 5) == 0) { s->p += 5; fm_json *v = fm_json_new(FMJ_BOOL); v->b = 0; return v; } if (s->end - s->p >= 4 && strncmp(s->p, "null", 4) == 0) { s->p += 4; return fm_json_new(FMJ_NULL); } if (c == '-' || (c >= '0' && c <= '9')) { const char *start = s->p; if (c == '-') s->p++; if (s->p >= s->end || *s->p < '0' || *s->p > '9') { s->err = 1; return NULL; } if (*s->p == '0' && s->p + 1 < s->end && s->p[1] >= '0' && s->p[1] <= '9') { s->err = 1; /* leading zero */ return NULL; } while (s->p < s->end && *s->p >= '0' && *s->p <= '9') s->p++; int is_int = 1; if (s->p < s->end && *s->p == '.') { is_int = 0; s->p++; if (s->p >= s->end || *s->p < '0' || *s->p > '9') { s->err = 1; return NULL; } while (s->p < s->end && *s->p >= '0' && *s->p <= '9') s->p++; } if (s->p < s->end && (*s->p == 'e' || *s->p == 'E')) { is_int = 0; s->p++; if (s->p < s->end && (*s->p == '+' || *s->p == '-')) s->p++; if (s->p >= s->end || *s->p < '0' || *s->p > '9') { s->err = 1; return NULL; } while (s->p < s->end && *s->p >= '0' && *s->p <= '9') s->p++; } char *endp = NULL; errno = 0; double d = strtod(start, &endp); if (errno != 0 || endp == start) { s->err = 1; return NULL; } fm_json *v = fm_json_new(FMJ_NUMBER); v->num = d; if (is_int && d >= -9.0e18 && d <= 9.0e18) { v->is_int = 1; v->i = (long long)d; } return v; } s->err = 1; return NULL; } static inline fm_json *fm_json_parse(const char *text) { if (!text) return NULL; fm_jp s = {text, text + strlen(text), 0, 0}; fm_json *v = fm_jvalue(&s); if (!v) return NULL; fm_jskip(&s); if (s.p != s.end) { /* trailing garbage */ fm_json_free(v); return NULL; } return v; } static inline void fm_jstring_out(fm_buf *b, const char *s) { fm_buf_push(b, '"'); for (const unsigned char *p = (const unsigned char *)s; *p; p++) { unsigned char c = *p; switch (c) { case '"': fm_buf_put(b, "\\\""); break; case '\\': fm_buf_put(b, "\\\\"); break; case '\b': fm_buf_put(b, "\\b"); break; case '\f': fm_buf_put(b, "\\f"); break; case '\n': fm_buf_put(b, "\\n"); break; case '\r': fm_buf_put(b, "\\r"); break; case '\t': fm_buf_put(b, "\\t"); break; default: if (c < 0x20) fm_buf_fmt(b, "\\u%04x", c); else fm_buf_push(b, (char)c); } } fm_buf_push(b, '"'); } static inline void fm_json_out(const fm_json *v, fm_buf *b) { switch (v->type) { case FMJ_NULL: fm_buf_put(b, "null"); break; case FMJ_BOOL: fm_buf_put(b, v->b ? "true" : "false"); break; case FMJ_STRING: fm_jstring_out(b, v->str ? v->str : ""); break; case FMJ_NUMBER: if (v->is_int) fm_buf_fmt(b, "%lld", v->i); else fm_buf_fmt(b, "%.15g", v->num); break; case FMJ_ARRAY: fm_buf_push(b, '['); for (size_t i = 0; i < v->count; i++) { if (i) fm_buf_put(b, ","); fm_json_out(v->items[i], b); } fm_buf_push(b, ']'); break; case FMJ_OBJECT: fm_buf_push(b, '{'); for (size_t i = 0; i < v->count; i++) { if (i) fm_buf_put(b, ","); fm_jstring_out(b, v->items[i]->key ? v->items[i]->key : ""); fm_buf_put(b, ":"); fm_json_out(v->items[i], b); } fm_buf_push(b, '}'); break; } } static inline char *fm_json_serialize(const fm_json *v) { fm_buf b; fm_buf_init(&b); if (v) fm_json_out(v, &b); return fm_buf_steal(&b); } /* Shallow merge of two objects: union of keys, `over` wins per key. * Takes ownership of both inputs (either may be NULL or non-object, in * which case it is dropped). Returns a new object; free with fm_json_free. * Member order: over's keys first, then base-only keys. */ static inline fm_json *fm_json_merge_owned(const fm_json *base, const fm_json *over) { fm_json *B = (base && base->type == FMJ_OBJECT) ? (fm_json *)base : NULL; fm_json *O = (over && over->type == FMJ_OBJECT) ? (fm_json *)over : NULL; if (base && base->type != FMJ_OBJECT) fm_json_free((fm_json *)base); if (over && over->type != FMJ_OBJECT) fm_json_free((fm_json *)over); fm_json *r = fm_json_new(FMJ_OBJECT); if (O) { for (size_t i = 0; i < O->count; i++) if (O->items[i]) { r->items = (fm_json **)fm_xrealloc(r->items, (r->count + 1) * sizeof(fm_json *)); r->items[r->count++] = O->items[i]; } } if (B) { for (size_t i = 0; i < B->count; i++) { fm_json *m = B->items[i]; if (!m) continue; if (O && fm_json_get(O, m->key)) fm_json_free(m); /* superseded */ else { r->items = (fm_json **)fm_xrealloc(r->items, (r->count + 1) * sizeof(fm_json *)); r->items[r->count++] = m; } } } if (B) { free(B->items); free(B->key); free(B->str); free(B); } if (O) { free(O->items); free(O->key); free(O->str); free(O); } return r; } /* ------------------------------------------------------------------ */ /* Frontmatter */ /* ------------------------------------------------------------------ */ /* Frontmatter value */ typedef enum { FM_STR, FM_LIST, FM_BOOL, FM_INT, } fm_kind; typedef struct { fm_kind kind; char *s; char **items; size_t item_count; bool b; long long i; } fm_value; static inline void fm_value_free(fm_value *v) { free(v->s); for (size_t i = 0; i < v->item_count; i++) free(v->items[i]); free(v->items); memset(v, 0, sizeof *v); } /* List-valued field as items: JSON array as-is, CSV split on commas, * single string as one item. */ static inline void fm_list_items(const fm_value *v, fm_strlist *out) { fm_sl_init(out); if (!v) return; if (v->kind == FM_LIST) { for (size_t i = 0; i < v->item_count; i++) fm_sl_push_copy(out, v->items[i]); return; } if (v->kind != FM_STR || !v->s || !strchr(v->s, ',')) { if (v->s) { size_t a = 0, n = strlen(v->s); while (a < n && isspace((unsigned char)v->s[a])) a++; while (n > a && isspace((unsigned char)v->s[n - 1])) n--; if (n > a) fm_sl_push(out, fm_strndup(v->s + a, n - a)); } return; } const char *s = v->s; size_t n = strlen(s); size_t start = 0; for (;;) { while (start < n && isspace((unsigned char)s[start])) start++; if (start >= n) break; const char *comma = (const char *)memchr(s + start, ',', n - start); size_t seg_end = comma ? (size_t)(comma - s) : n; size_t a = start, b = seg_end; while (b > a && isspace((unsigned char)s[b - 1])) b--; if (b > a) fm_sl_push(out, fm_strndup(s + a, b - a)); if (!comma) break; start = seg_end + 1; } } /* A frontmatter entry */ typedef struct { char *key; fm_value val; } fm_entry; /* A markdown document with optional `---` frontmatter */ typedef struct { fm_entry *entries; size_t count; size_t cap; char *body; } doc; static inline const fm_value *doc_get(const doc *d, const char *key) { for (size_t i = 0; i < d->count; i++) if (strcmp(d->entries[i].key, key) == 0) return &d->entries[i].val; return NULL; } static inline void doc_free(doc *d) { for (size_t i = 0; i < d->count; i++) { free(d->entries[i].key); fm_value_free(&d->entries[i].val); } free(d->entries); free(d->body); memset(d, 0, sizeof *d); } static inline void fm_parse_value(const char *raw, fm_value *v) { memset(v, 0, sizeof *v); if (raw[0] == '[') { fm_json *arr = fm_json_parse(raw); if (arr && arr->type == FMJ_ARRAY) { v->kind = FM_LIST; v->items = (char **)fm_xmalloc(arr->count * sizeof(char *)); size_t i = 0; for (size_t k = 0; k < arr->count; k++) { const fm_json *it = arr->items[k]; if (it && it->type == FMJ_STRING) v->items[i++] = fm_strdup(it->str); else v->items[i++] = it ? fm_json_serialize(it) : fm_strdup(""); } v->item_count = arr->count; fm_json_free(arr); return; } fm_json_free(arr); v->kind = FM_STR; v->s = fm_strdup(raw); return; } if (strcmp(raw, "true") == 0) { v->kind = FM_BOOL; v->b = true; return; } if (strcmp(raw, "false") == 0) { v->kind = FM_BOOL; v->b = false; return; } size_t len = strlen(raw); if (len >= 2 && ((raw[0] == '"' && raw[len - 1] == '"') || (raw[0] == '\'' && raw[len - 1] == '\''))) { v->kind = FM_STR; v->s = fm_strndup(raw + 1, len - 2); return; } /* integer? */ char *end = NULL; errno = 0; long long n = strtoll(raw, &end, 10); if (errno == 0 && end && *end == '\0' && raw[0] != '\0') { v->kind = FM_INT; v->i = n; return; } v->kind = FM_STR; v->s = fm_strdup(raw); } /* Split text into lines (no trailing empty). Returns line count. */ static inline char **fm_split_lines(const char *text, size_t *out_count) { size_t cap = 16, n = 0; char **lines = (char **)fm_xmalloc(cap * sizeof(char *)); const char *p = text; while (*p) { const char *nl = (const char *)strchr(p, '\n'); size_t len = nl ? (size_t)(nl - p) : strlen(p); if (n + 1 > cap) { cap *= 2; lines = (char **)fm_xrealloc(lines, cap * sizeof(char *)); } lines[n++] = fm_strndup(p, len); if (!nl) break; p = nl + 1; } *out_count = n; return lines; } static inline void parse_frontmatter(const char *text, doc *out) { memset(out, 0, sizeof *out); size_t nlines = 0; char **lines = fm_split_lines(text, &nlines); char *body = fm_strdup(text); /* default: whole text */ if (nlines == 0) { /* keep as-is */ } else if (strcmp(lines[0], "---") != 0) { /* trim end */ size_t blen = strlen(body); while (blen && (body[blen - 1] == '\n' || body[blen - 1] == '\r' || body[blen - 1] == ' ')) body[--blen] = '\0'; } else { size_t close = 0; bool found = false; for (size_t i = 1; i < nlines; i++) { if (strcmp(lines[i], "---") == 0) { close = i - 1; /* index within lines[1..] */ found = true; break; } } if (!found) { /* whole text is body, trimmed */ size_t blen = strlen(body); while (blen && (body[blen - 1] == '\n' || body[blen - 1] == '\r' || body[blen - 1] == ' ')) body[--blen] = '\0'; } else { for (size_t i = 1; i <= close; i++) { const char *line = lines[i]; /* trim */ size_t start = 0, len = strlen(line); while (start < len && isspace((unsigned char)line[start])) start++; while (len > start && isspace((unsigned char)line[len - 1])) len--; if (len == 0 || line[start] == '#') continue; const char *colon = (const char *)memchr(line + start, ':', len - start); if (!colon) continue; size_t klen = (size_t)(colon - (line + start)); if (klen == 0) continue; char *key = fm_strndup(line + start, klen); const char *vraw = colon + 1; /* trim value */ size_t vlen = strlen(vraw); size_t vstart = 0; while (vstart < vlen && isspace((unsigned char)vraw[vstart])) vstart++; while (vlen > vstart && isspace((unsigned char)vraw[vlen - 1])) vlen--; fm_value val; char *vtmp = fm_strndup(vraw + vstart, vlen - vstart); fm_parse_value(vtmp, &val); free(vtmp); if (out->count + 1 > out->cap) { out->cap = out->cap ? out->cap * 2 : 8; out->entries = (fm_entry *)fm_xrealloc(out->entries, out->cap * sizeof(out->entries[0])); } out->entries[out->count].key = key; out->entries[out->count].val = val; out->count++; } /* body = lines[2+close..] joined by \n, trimmed end */ fm_buf b; fm_buf_init(&b); for (size_t i = 2 + close; i < nlines; i++) { if (i > 2 + close) fm_buf_push(&b, '\n'); fm_buf_put(&b, lines[i]); } char *nb = fm_buf_steal(&b); size_t blen = strlen(nb); while (blen && (nb[blen - 1] == '\n' || nb[blen - 1] == '\r' || nb[blen - 1] == ' ')) nb[--blen] = '\0'; free(body); body = nb; } } for (size_t i = 0; i < nlines; i++) free(lines[i]); free(lines); out->body = body; } #ifdef __cplusplus } #endif #endif /* FRONTMATTER_H */