// SPDX-FileCopyrightText: © 2026 Vladimir Zorin // SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later // Licensed under OWL v1.0+. See LICENSE. #include "mneme.h" #include #include #include /* ── Default text analyzer ────────────────────────────────────────── */ /* Word characters: ASCII alphanumeric + UTF-8 continuation bytes (0x80-0xFF are treated as part of the current word so that multi-byte UTF-8 sequences are preserved intact). */ static inline int is_word_byte(uint8_t c) { if (c >= 'a' && c <= 'z') return 1; if (c >= 'A' && c <= 'Z') return 1; if (c >= '0' && c <= '9') return 1; if (c >= 0x80) return 1; /* UTF-8 continuation / lead */ return 0; } static inline uint8_t to_lower(uint8_t c) { if (c >= 'A' && c <= 'Z') return c + 32; return c; } int mneme_ft_analyze(const uint8_t *text, uint32_t text_len, mneme_ft_terms_t *out) { out->terms = NULL; out->buf = NULL; out->count = 0; out->cap = 0; if (!text || text_len == 0) return 0; /* Worst case: every byte is a separate 1-char token (but we skip tokens < MIN_TERM_LEN, so actual count is always less). Allocate conservatively: term array + copy buffer. */ int init_cap = 128; out->terms = (mneme_ft_term_t *)malloc((size_t)init_cap * sizeof(mneme_ft_term_t)); if (!out->terms) return -1; out->cap = init_cap; /* Buffer for lowercased term copies. Worst case = text_len bytes. */ out->buf = (uint8_t *)malloc(text_len); if (!out->buf) { free(out->terms); out->terms = NULL; return -1; } uint32_t buf_pos = 0; uint32_t i = 0; while (i < text_len) { /* Skip non-word bytes */ while (i < text_len && !is_word_byte(text[i])) i++; if (i >= text_len) break; /* Start of a word */ uint32_t start = buf_pos; uint32_t raw_len = 0; while (i < text_len && is_word_byte(text[i])) { if (raw_len < MNEME_FT_MAX_TERM_LEN) { out->buf[buf_pos++] = to_lower(text[i]); raw_len++; } i++; } /* Skip tokens shorter than minimum */ if (raw_len < MNEME_FT_MIN_TERM_LEN) { buf_pos = start; /* reclaim buffer space */ continue; } /* Enforce max terms limit */ if (out->count >= MNEME_FT_MAX_TERMS) break; /* Grow term array if needed */ if (out->count >= out->cap) { int new_cap = out->cap * 2; mneme_ft_term_t *tmp = (mneme_ft_term_t *)realloc(out->terms, (size_t)new_cap * sizeof(mneme_ft_term_t)); if (!tmp) break; out->terms = tmp; out->cap = new_cap; } out->terms[out->count].term = out->buf + start; out->terms[out->count].len = (uint16_t)raw_len; out->count++; } return 0; } void mneme_ft_terms_free(mneme_ft_terms_t *t) { if (t->buf) { free(t->buf); t->buf = NULL; } if (t->terms) { free(t->terms); t->terms = NULL; } t->count = 0; t->cap = 0; }