aboutsummaryrefslogtreecommitdiff
path: root/src/pms_ref.c
diff options
context:
space:
mode:
authorMiguel Dalberto Pedro <miguel.pedro@precisioninno.com>2026-10-29 17:55:36 -0300
committerMiguel Dalberto Pedro <miguel.pedro@precisioninno.com>2026-10-29 17:55:36 -0300
commit915478aef931cb57fe56fe568ab4982454a29812 (patch)
tree33057f4773852d99e8cd7ad92d568e79f34847ee /src/pms_ref.c
downloadpms-master.tar.gz
pms-master.zip
Initial commit: Padre Matos Soares (1932) Bible corpus and pms CLIHEADmaster
Diffstat (limited to 'src/pms_ref.c')
-rw-r--r--src/pms_ref.c326
1 files changed, 326 insertions, 0 deletions
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 <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+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: /<pattern>, <Book>/<pattern>, <Book>:?<Chapter>/<pattern> */
+ 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. <Book> 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: <Chapter>:<Verse>-<Chapter>:<Verse> */
+ 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: <Chapter>:<V1>,<V2>... */
+ 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: <Chapter>:<Verse>-<Verse> */
+ 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: <Chapter>:<Verse> or <Chapter> <Verse> */
+ 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: <Chapter>-<Chapter> */
+ 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: <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;
+}