From 915478aef931cb57fe56fe568ab4982454a29812 Mon Sep 17 00:00:00 2001 From: Miguel Dalberto Pedro Date: Thu, 29 Oct 2026 17:55:36 -0300 Subject: Initial commit: Padre Matos Soares (1932) Bible corpus and pms CLI --- src/pms_ref.c | 326 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 src/pms_ref.c (limited to 'src/pms_ref.c') diff --git a/src/pms_ref.c b/src/pms_ref.c new file mode 100644 index 0000000..45a4f16 --- /dev/null +++ b/src/pms_ref.c @@ -0,0 +1,326 @@ +#include "pms_ref.h" +#include "pms_data.h" +#include "pms_util.h" +#include +#include +#include +#include + +pms_ref *pms_newref(void) { + return calloc(1, sizeof(pms_ref)); +} + +void pms_freeref(pms_ref *ref) { + if (!ref) { + return; + } + if (ref->search_str) { + free(ref->search_str); + regfree(&ref->search); + } + free(ref); +} + +static bool alias_list_matches(const char *aliases, const char *key, bool prefix_ok) { + char buf[256]; + strncpy(buf, aliases, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + + char *saveptr = NULL; + char *tok = strtok_r(buf, ",", &saveptr); + size_t key_len = strlen(key); + while (tok) { + if (strcmp(tok, key) == 0) { + return true; + } + if (prefix_ok && key_len >= 2 && strncmp(tok, key, key_len) == 0) { + return true; + } + tok = strtok_r(NULL, ",", &saveptr); + } + return false; +} + +int pms_book_from_name(const char *name) { + char key[128]; + if (pms_normalize_book_key(name, key, sizeof(key)) == 0) { + return 0; + } + size_t key_len = strlen(key); + + /* Pass 1: exact match on normalized abbr or name */ + for (int i = 0; i < pms_books_length; i++) { + char norm_abbr[64], norm_name[128]; + pms_normalize_book_key(pms_books[i].abbr, norm_abbr, sizeof(norm_abbr)); + pms_normalize_book_key(pms_books[i].name, norm_name, sizeof(norm_name)); + if (strcmp(key, norm_abbr) == 0 || strcmp(key, norm_name) == 0) { + return pms_books[i].number; + } + } + + /* Pass 2: exact match on aliases */ + for (int i = 0; i < pms_books_length; i++) { + if (alias_list_matches(pms_books[i].aliases, key, false)) { + return pms_books[i].number; + } + } + + /* Pass 3: prefix match on normalized name, abbr, or aliases */ + for (int i = 0; i < pms_books_length; i++) { + char norm_abbr[64], norm_name[128]; + pms_normalize_book_key(pms_books[i].abbr, norm_abbr, sizeof(norm_abbr)); + pms_normalize_book_key(pms_books[i].name, norm_name, sizeof(norm_name)); + if (strncmp(norm_name, key, key_len) == 0 || strncmp(norm_abbr, key, key_len) == 0) { + return pms_books[i].number; + } + if (alias_list_matches(pms_books[i].aliases, key, true)) { + return pms_books[i].number; + } + } + + /* Pass 4: substring match on normalized name (for "Mateus" -> "S. Mateus", etc.) */ + if (!isdigit((unsigned char)key[0])) { + for (int i = 0; i < pms_books_length; i++) { + char norm_name[128]; + pms_normalize_book_key(pms_books[i].name, norm_name, sizeof(norm_name)); + if (!isdigit((unsigned char)norm_name[0]) && strstr(norm_name, key) != NULL) { + return pms_books[i].number; + } + } + } + + return 0; +} + +/* + * Split a string like "1 Reis 3:16-18" or "Gn1:1" or "S. João 3" into + * book_out ("1 Reis", "Gn", "S. João") and spec_out ("3:16-18", "1:1", "3"). + */ +static void split_book_and_spec(const char *s, char *book_out, size_t book_sz, char *spec_out, size_t spec_sz) { + while (*s && isspace((unsigned char)*s)) { + s++; + } + const char *p = s; + + /* Optional leading book number (1, 2, 3, 4 or Roman I, II, III, IV) */ + if (isdigit((unsigned char)*p)) { + while (isdigit((unsigned char)*p)) { + p++; + } + while (*p && (isspace((unsigned char)*p) || *p == '.')) { + p++; + } + } + + /* Consume book name characters (letters, spaces, dots, parentheses, hyphens in book names, UTF-8 bytes) */ + while (*p) { + unsigned char c = (unsigned char)*p; + if (c == ':' || c == '/') { + break; + } + if (isdigit(c)) { + break; + } + p++; + } + + size_t blen = (size_t)(p - s); + while (blen > 0 && (isspace((unsigned char)s[blen - 1]) || s[blen - 1] == ':')) { + blen--; + } + if (blen >= book_sz) { + blen = book_sz - 1; + } + memcpy(book_out, s, blen); + book_out[blen] = '\0'; + + while (*p && (isspace((unsigned char)*p) || *p == ':')) { + p++; + } + strncpy(spec_out, p, spec_sz - 1); + spec_out[spec_sz - 1] = '\0'; + /* Trim trailing whitespace from spec_out */ + size_t slen = strlen(spec_out); + while (slen > 0 && isspace((unsigned char)spec_out[slen - 1])) { + spec_out[--slen] = '\0'; + } +} + +static int parse_verse_set(const char *vspec, pms_ref *ref) { + char buf[256]; + strncpy(buf, vspec, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + + ref->verse_set_count = 0; + char *saveptr = NULL; + char *tok = strtok_r(buf, ",", &saveptr); + while (tok) { + while (*tok && isspace((unsigned char)*tok)) tok++; + if (*tok == '\0') { + tok = strtok_r(NULL, ",", &saveptr); + continue; + } + unsigned int v1 = 0, v2 = 0; + char extra = 0; + if (sscanf(tok, "%u - %u %c", &v1, &v2, &extra) == 2) { + if (v1 == 0 || v2 < v1) return -1; + for (unsigned int v = v1; v <= v2 && ref->verse_set_count < PMS_MAX_VERSE_SET; v++) { + ref->verse_set[ref->verse_set_count++] = v; + } + } else if (sscanf(tok, "%u %c", &v1, &extra) == 1) { + if (v1 == 0) return -1; + if (ref->verse_set_count < PMS_MAX_VERSE_SET) { + ref->verse_set[ref->verse_set_count++] = v1; + } + } else { + return -1; + } + tok = strtok_r(NULL, ",", &saveptr); + } + return ref->verse_set_count > 0 ? 0 : -1; +} + +int pms_parse_ref(const char *input, pms_ref *ref) { + if (!input || !ref) { + return -1; + } + memset(ref, 0, sizeof(*ref)); + + while (*input && isspace((unsigned char)*input)) { + input++; + } + if (*input == '\0') { + return -1; + } + + /* Check for '/' search syntax: /, /, :?/ */ + const char *slash = strchr(input, '/'); + if (slash != NULL) { + const char *pattern = slash + 1; + if (*pattern == '\0') { + return -1; + } + char prefix[256]; + size_t plen = (size_t)(slash - input); + if (plen >= sizeof(prefix)) plen = sizeof(prefix) - 1; + memcpy(prefix, input, plen); + prefix[plen] = '\0'; + + if (plen > 0) { + char book_part[128], spec_part[128]; + split_book_and_spec(prefix, book_part, sizeof(book_part), spec_part, sizeof(spec_part)); + if (book_part[0] != '\0') { + int bnum = pms_book_from_name(book_part); + if (bnum == 0) { + return -1; + } + ref->book = (unsigned int)bnum; + } + if (spec_part[0] != '\0') { + unsigned int ch = 0; + char extra = 0; + if (sscanf(spec_part, "%u %c", &ch, &extra) != 1 || ch == 0) { + return -1; + } + ref->chapter = ch; + } + } + + char folded_pat[512]; + pms_fold_utf8(pattern, folded_pat, sizeof(folded_pat), NULL); + ref->search_str = strdup(folded_pat); + if (!ref->search_str) { + return -1; + } + if (regcomp(&ref->search, ref->search_str, REG_EXTENDED | REG_ICASE) != 0) { + free(ref->search_str); + ref->search_str = NULL; + return -1; + } + ref->type = PMS_REF_SEARCH; + return 0; + } + + /* Standard passage reference */ + char book_part[128], spec_part[128]; + split_book_and_spec(input, book_part, sizeof(book_part), spec_part, sizeof(spec_part)); + if (book_part[0] == '\0') { + return -1; + } + int bnum = pms_book_from_name(book_part); + if (bnum == 0) { + return -1; + } + ref->book = (unsigned int)bnum; + + /* 1. only */ + if (spec_part[0] == '\0') { + ref->type = PMS_REF_EXACT; + return 0; + } + + unsigned int c1 = 0, c2 = 0, v1 = 0, v2 = 0; + char extra = 0; + + /* 2. Cross-chapter range: :-: */ + if (sscanf(spec_part, "%u : %u - %u : %u %c", &c1, &v1, &c2, &v2, &extra) == 4) { + if (c1 == 0 || v1 == 0 || c2 < c1 || v2 == 0) return -1; + ref->type = PMS_REF_RANGE_EXT; + ref->chapter = c1; + ref->verse = v1; + ref->chapter_end = c2; + ref->verse_end = v2; + return 0; + } + + /* 3. Comma-separated verse set: :,... */ + const char *colon = strchr(spec_part, ':'); + if (colon != NULL && strchr(colon + 1, ',') != NULL) { + if (sscanf(spec_part, "%u :", &c1) != 1 || c1 == 0) { + return -1; + } + ref->chapter = c1; + ref->type = PMS_REF_EXACT_SET; + return parse_verse_set(colon + 1, ref); + } + + /* 4. Single-chapter verse range: :- */ + if (sscanf(spec_part, "%u : %u - %u %c", &c1, &v1, &v2, &extra) == 3 || + sscanf(spec_part, "%u %u - %u %c", &c1, &v1, &v2, &extra) == 3) { + if (c1 == 0 || v1 == 0 || v2 < v1) return -1; + ref->type = PMS_REF_RANGE; + ref->chapter = c1; + ref->verse = v1; + ref->verse_end = v2; + return 0; + } + + /* 5. Exact chapter and verse: : or */ + if (sscanf(spec_part, "%u : %u %c", &c1, &v1, &extra) == 2 || + sscanf(spec_part, "%u %u %c", &c1, &v1, &extra) == 2) { + if (c1 == 0 || v1 == 0) return -1; + ref->type = PMS_REF_EXACT; + ref->chapter = c1; + ref->verse = v1; + return 0; + } + + /* 6. Chapter range: - */ + if (sscanf(spec_part, "%u - %u %c", &c1, &c2, &extra) == 2) { + if (c1 == 0 || c2 < c1) return -1; + ref->type = PMS_REF_RANGE; + ref->chapter = c1; + ref->chapter_end = c2; + return 0; + } + + /* 7. Whole chapter: */ + if (sscanf(spec_part, "%u %c", &c1, &extra) == 1) { + if (c1 == 0) return -1; + ref->type = PMS_REF_EXACT; + ref->chapter = c1; + return 0; + } + + return -1; +} -- cgit v1.2.3