#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; }