From f8423ebea413605b42de23f5dd06c5479d3fa1dd Mon Sep 17 00:00:00 2001 From: Ethan Werner Date: Sat, 10 Oct 2026 20:55:33 +0000 Subject: initial commit --- include/frontmatter.h | 1058 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1058 insertions(+) create mode 100644 include/frontmatter.h (limited to 'include/frontmatter.h') diff --git a/include/frontmatter.h b/include/frontmatter.h new file mode 100644 index 0000000..56c6935 --- /dev/null +++ b/include/frontmatter.h @@ -0,0 +1,1058 @@ +/* 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 */ -- cgit v1.3.1