diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/pms_config.h | 16 | ||||
| -rw-r--r-- | src/pms_data.h | 36 | ||||
| -rw-r--r-- | src/pms_main.c | 297 | ||||
| -rw-r--r-- | src/pms_match.c | 142 | ||||
| -rw-r--r-- | src/pms_match.h | 20 | ||||
| -rw-r--r-- | src/pms_ref.c | 326 | ||||
| -rw-r--r-- | src/pms_ref.h | 37 | ||||
| -rw-r--r-- | src/pms_render.c | 306 | ||||
| -rw-r--r-- | src/pms_render.h | 10 | ||||
| -rw-r--r-- | src/pms_util.c | 89 | ||||
| -rw-r--r-- | src/pms_util.h | 27 |
11 files changed, 1306 insertions, 0 deletions
diff --git a/src/pms_config.h b/src/pms_config.h new file mode 100644 index 0000000..8801d9d --- /dev/null +++ b/src/pms_config.h @@ -0,0 +1,16 @@ +#ifndef PMS_CONFIG_H +#define PMS_CONFIG_H + +#include <stdbool.h> + +typedef struct { + int before_context; + int after_context; + bool chapter_context; + bool show_notes; + bool highlighting; + bool pretty; + int maximum_line_length; +} pms_config; + +#endif /* PMS_CONFIG_H */ diff --git a/src/pms_data.h b/src/pms_data.h new file mode 100644 index 0000000..a9eefe5 --- /dev/null +++ b/src/pms_data.h @@ -0,0 +1,36 @@ +#ifndef PMS_DATA_H +#define PMS_DATA_H + +typedef struct { + int number; + const char *name; + const char *abbr; + const char *aliases; /* comma-separated normalized aliases */ +} pms_book; + +typedef struct { + int book; + int chapter; + int verse; + const char *text; +} pms_verse; + +typedef struct { + int book; + int chapter; + int verse_start; + int verse_end; + const char *label; + const char *text; +} pms_note; + +extern pms_book pms_books[]; +extern int pms_books_length; + +extern pms_verse pms_verses[]; +extern int pms_verses_length; + +extern pms_note pms_notes[]; +extern int pms_notes_length; + +#endif /* PMS_DATA_H */ diff --git a/src/pms_main.c b/src/pms_main.c new file mode 100644 index 0000000..1924b6a --- /dev/null +++ b/src/pms_main.c @@ -0,0 +1,297 @@ +#include "pms_config.h" +#include "pms_data.h" +#include "pms_ref.h" +#include "pms_render.h" +#include <ctype.h> +#include <errno.h> +#include <signal.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/wait.h> +#include <unistd.h> + +static const char *usage_str = + "usage: pms [flags] [reference...]\n" + "\n" + "Bíblia Sagrada — Tradução e Comentários do Padre Manuel de Matos Soares (1932)\n" + "\n" + "Flags:\n" + " -A num show num verses of context after matching verses\n" + " -B num show num verses of context before matching verses\n" + " -C show matching verses in context of the chapter\n" + " -c show Padre Matos Soares's commentaries/footnotes for matching verses\n" + " -e highlighting of chapters and verse numbers\n" + " (default when output is a TTY)\n" + " -p output to less with chapter grouping, spacing, indentation,\n" + " and line wrapping\n" + " (default when output is a TTY)\n" + " -W no line wrap\n" + " -l list books\n" + " -h show help\n" + "\n" + "Reference:\n" + " <Book>\n" + " Individual book (e.g. \"Gn\", \"1Rs\", \"1Sm\", \"Mt\")\n" + " <Book>:<Chapter> or <Book> <Chapter>\n" + " Individual chapter of a book (e.g. \"Gn:1\", \"Sl 23\")\n" + " <Book>:<Chapter>:<Verse>[,<Verse>]...\n" + " Individual verse(s) of a specific chapter of a book (e.g. \"Jo 3:16\", \"Gn 1:1,3,5\")\n" + " <Book>:<Chapter>-<Chapter>\n" + " Range of chapters in a book (e.g. \"Gn 1-3\")\n" + " <Book>:<Chapter>:<Verse>-<Verse>\n" + " Range of verses in a book chapter (e.g. \"Gn 1:1-5\")\n" + " <Book>:<Chapter>:<Verse>-<Chapter>:<Verse>\n" + " Range of chapters and verses in a book (e.g. \"Gn 1:26-2:3\")\n" + "\n" + " /<Search>\n" + " All verses that match a pattern (case- and accent-insensitive)\n" + " <Book>/<Search>\n" + " All verses in a book that match a pattern\n" + " <Book>:<Chapter>/<Search>\n" + " All verses in a chapter of a book that match a pattern\n"; + +static int get_terminal_width(void) { + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) { + return (ws.ws_col < 80) ? ws.ws_col : 80; + } + return 80; +} + +static FILE *open_pager(pid_t *pager_pid) { + *pager_pid = -1; + int pipefd[2]; + if (pipe(pipefd) != 0) { + return stdout; + } + + pid_t pid = fork(); + if (pid < 0) { + close(pipefd[0]); + close(pipefd[1]); + return stdout; + } + + if (pid == 0) { + /* Child: run pager */ + close(pipefd[1]); + if (dup2(pipefd[0], STDIN_FILENO) < 0) { + _exit(1); + } + close(pipefd[0]); + + const char *pager = getenv("PAGER"); + if (pager && pager[0] != '\0') { + execlp(pager, pager, (char *)NULL); + } + execlp("less", "less", "-J", "-I", "-R", "-f", "-", (char *)NULL); + execlp("more", "more", (char *)NULL); + _exit(1); + } + + /* Parent */ + close(pipefd[0]); + *pager_pid = pid; + FILE *fp = fdopen(pipefd[1], "w"); + if (!fp) { + close(pipefd[1]); + *pager_pid = -1; + return stdout; + } + return fp; +} + +static void close_pager(FILE *fp, pid_t pager_pid) { + if (fp && fp != stdout) { + fclose(fp); + } + if (pager_pid > 0) { + int status = 0; + waitpid(pager_pid, &status, 0); + } +} + +static int run_query(const char *query_str, const pms_config *config, bool use_pager) { + pms_ref *ref = pms_newref(); + if (!ref) { + return 1; + } + if (pms_parse_ref(query_str, ref) != 0) { + fprintf(stderr, "pms: referência inválida: %s\n", query_str); + pms_freeref(ref); + return 1; + } + + pid_t pager_pid = -1; + FILE *out = stdout; + if (use_pager) { + out = open_pager(&pager_pid); + } + + pms_render(ref, config, out); + + if (use_pager) { + close_pager(out, pager_pid); + } + pms_freeref(ref); + return 0; +} + +int main(int argc, char *argv[]) { + bool is_tty = isatty(STDOUT_FILENO) != 0; + signal(SIGPIPE, SIG_IGN); + + pms_config config = { + .before_context = 0, + .after_context = 0, + .chapter_context = false, + .show_notes = false, + .highlighting = is_tty, + .pretty = is_tty, + .maximum_line_length = get_terminal_width(), + }; + + bool list_books = false; + bool no_wrap = false; + + int opt; + while ((opt = getopt(argc, argv, "A:B:CcepwWlhn")) != -1) { + switch (opt) { + case 'A': { + char *endptr = NULL; + errno = 0; + long val = strtol(optarg, &endptr, 10); + if (errno != 0 || endptr == optarg || *endptr != '\0' || val < 0) { + fprintf(stderr, "pms: número inválido para -A: %s\n\n%s", optarg, usage_str); + return 1; + } + config.after_context = (int)val; + break; + } + case 'B': { + char *endptr = NULL; + errno = 0; + long val = strtol(optarg, &endptr, 10); + if (errno != 0 || endptr == optarg || *endptr != '\0' || val < 0) { + fprintf(stderr, "pms: número inválido para -B: %s\n\n%s", optarg, usage_str); + return 1; + } + config.before_context = (int)val; + break; + } + case 'C': + config.chapter_context = true; + break; + case 'c': + case 'n': + config.show_notes = true; + break; + case 'e': + config.highlighting = true; + break; + case 'p': + config.pretty = true; + break; + case 'W': + case 'w': + no_wrap = true; + break; + case 'l': + list_books = true; + break; + case 'h': + printf("%s", usage_str); + return 0; + default: + fprintf(stderr, "\n%s", usage_str); + return 1; + } + } + + if (no_wrap) { + config.maximum_line_length = 0; + } + + if (list_books) { + for (int i = 0; i < pms_books_length; i++) { + printf("%s (%s)\n", pms_books[i].name, pms_books[i].abbr); + } + return 0; + } + + int rem_argc = argc - optind; + char **rem_argv = argv + optind; + + if (rem_argc == 0) { + /* Interactive REPL mode */ + char line[1024]; + while (true) { + if (isatty(STDIN_FILENO)) { + printf("pms> "); + fflush(stdout); + } + if (!fgets(line, sizeof(line), stdin)) { + if (isatty(STDIN_FILENO)) { + printf("\n"); + } + break; + } + size_t len = strlen(line); + while (len > 0 && isspace((unsigned char)line[len - 1])) { + line[--len] = '\0'; + } + char *p = line; + while (*p && isspace((unsigned char)*p)) { + p++; + } + if (*p == '\0') { + continue; + } + if (strcmp(p, "q") == 0 || strcmp(p, "quit") == 0 || + strcmp(p, "exit") == 0 || strcmp(p, "sair") == 0) { + break; + } + if (strcmp(p, ":c") == 0 || strcmp(p, "-c") == 0) { + config.show_notes = !config.show_notes; + printf("Comentários: %s\n", config.show_notes ? "activados" : "desactivados"); + continue; + } + if (strcmp(p, "-l") == 0 || strcmp(p, ":l") == 0) { + for (int i = 0; i < pms_books_length; i++) { + printf("%s (%s)\n", pms_books[i].name, pms_books[i].abbr); + } + continue; + } + if (strcmp(p, "-h") == 0 || strcmp(p, ":h") == 0 || strcmp(p, "help") == 0) { + printf("%s", usage_str); + continue; + } + run_query(p, &config, is_tty && config.pretty); + } + return 0; + } + + /* Join remaining CLI arguments into a single reference string */ + size_t total_len = 0; + for (int i = 0; i < rem_argc; i++) { + total_len += strlen(rem_argv[i]) + 2; + } + char *joined = malloc(total_len + 1); + if (!joined) { + return 1; + } + joined[0] = '\0'; + for (int i = 0; i < rem_argc; i++) { + if (i > 0) { + strcat(joined, " "); + } + strcat(joined, rem_argv[i]); + } + + int rc = run_query(joined, &config, is_tty && config.pretty); + free(joined); + return rc; +} diff --git a/src/pms_match.c b/src/pms_match.c new file mode 100644 index 0000000..8140dd5 --- /dev/null +++ b/src/pms_match.c @@ -0,0 +1,142 @@ +#include "pms_match.h" +#include "pms_data.h" +#include "pms_util.h" +#include <limits.h> +#include <stdbool.h> +#include <stddef.h> + +static bool verse_matches(const pms_ref *ref, const pms_verse *v) { + switch (ref->type) { + case PMS_REF_SEARCH: { + if (ref->book != 0 && (unsigned int)v->book != ref->book) { + return false; + } + if (ref->chapter != 0 && (unsigned int)v->chapter != ref->chapter) { + return false; + } + char folded[4096]; + pms_fold_utf8(v->text, folded, sizeof(folded), NULL); + return regexec(&ref->search, folded, 0, NULL, 0) == 0; + } + + case PMS_REF_EXACT: + return (unsigned int)v->book == ref->book && + (ref->chapter == 0 || (unsigned int)v->chapter == ref->chapter) && + (ref->verse == 0 || (unsigned int)v->verse == ref->verse); + + case PMS_REF_EXACT_SET: + if ((unsigned int)v->book != ref->book || (unsigned int)v->chapter != ref->chapter) { + return false; + } + for (int i = 0; i < ref->verse_set_count; i++) { + if ((unsigned int)v->verse == ref->verse_set[i]) { + return true; + } + } + return false; + + case PMS_REF_RANGE: + return (unsigned int)v->book == ref->book && + ((ref->chapter_end == 0 && (unsigned int)v->chapter == ref->chapter) || + ((unsigned int)v->chapter >= ref->chapter && (unsigned int)v->chapter <= ref->chapter_end)) && + (ref->verse == 0 || (unsigned int)v->verse >= ref->verse) && + (ref->verse_end == 0 || (unsigned int)v->verse <= ref->verse_end); + + case PMS_REF_RANGE_EXT: + return (unsigned int)v->book == ref->book && + (((unsigned int)v->chapter == ref->chapter && (unsigned int)v->verse >= ref->verse && ref->chapter != ref->chapter_end) || + ((unsigned int)v->chapter > ref->chapter && (unsigned int)v->chapter < ref->chapter_end) || + ((unsigned int)v->chapter == ref->chapter_end && (unsigned int)v->verse <= ref->verse_end && ref->chapter != ref->chapter_end) || + (ref->chapter == ref->chapter_end && (unsigned int)v->chapter == ref->chapter && + (unsigned int)v->verse >= ref->verse && (unsigned int)v->verse <= ref->verse_end)); + + default: + return false; + } +} + +static pms_range chapter_bounds(int i, const pms_config *config) { + pms_range bounds = {.start = i, .end = i + 1}; + int book = pms_verses[i].book; + int chapter = pms_verses[i].chapter; + + int max_before = config->chapter_context ? INT_MAX : config->before_context; + for (int n = 0; n < max_before && bounds.start > 0; n++) { + if (pms_verses[bounds.start - 1].book == book && + (!config->chapter_context || pms_verses[bounds.start - 1].chapter == chapter)) { + bounds.start--; + } else { + break; + } + } + + int max_after = config->chapter_context ? INT_MAX : config->after_context; + for (int n = 0; n < max_after && bounds.end < pms_verses_length; n++) { + if (pms_verses[bounds.end].book == book && + (!config->chapter_context || pms_verses[bounds.end].chapter == chapter)) { + bounds.end++; + } else { + break; + } + } + + return bounds; +} + +static int find_next_match(const pms_ref *ref, const pms_config *config, int i, pms_range *range) { + for (; i < pms_verses_length; i++) { + if (verse_matches(ref, &pms_verses[i])) { + *range = chapter_bounds(i, config); + return i; + } + } + return -1; +} + +int pms_next_verse(const pms_ref *ref, const pms_config *config, pms_match_data *data) { + if (data->current >= pms_verses_length) { + return -1; + } + + if (data->current == 0 && data->next_match == 0) { + data->matches[0] = (pms_range){-1, -1}; + data->matches[1] = (pms_range){-1, -1}; + + data->next_match = find_next_match(ref, config, 0, &data->matches[0]); + if (data->next_match < 0) { + data->current = pms_verses_length; + return -1; + } + data->next_match = find_next_match(ref, config, data->next_match + 1, &data->matches[1]); + } + + while (data->matches[1].start != -1 && data->matches[1].start <= data->matches[0].end) { + if (data->matches[1].end > data->matches[0].end) { + data->matches[0].end = data->matches[1].end; + } + data->matches[1] = (pms_range){-1, -1}; + if (data->next_match >= 0) { + data->next_match = find_next_match(ref, config, data->next_match + 1, &data->matches[1]); + } + } + + if (data->current < data->matches[0].start) { + data->current = data->matches[0].start; + } + + if (data->current < data->matches[0].end) { + return data->current++; + } + + if (data->matches[1].start != -1) { + data->matches[0] = data->matches[1]; + data->matches[1] = (pms_range){-1, -1}; + if (data->next_match >= 0) { + data->next_match = find_next_match(ref, config, data->next_match + 1, &data->matches[1]); + } + return pms_next_verse(ref, config, data); + } + + data->current = pms_verses_length; + return -1; +} diff --git a/src/pms_match.h b/src/pms_match.h new file mode 100644 index 0000000..89dec53 --- /dev/null +++ b/src/pms_match.h @@ -0,0 +1,20 @@ +#ifndef PMS_MATCH_H +#define PMS_MATCH_H + +#include "pms_config.h" +#include "pms_ref.h" + +typedef struct { + int start; + int end; +} pms_range; + +typedef struct { + int current; + int next_match; + pms_range matches[2]; +} pms_match_data; + +int pms_next_verse(const pms_ref *ref, const pms_config *config, pms_match_data *data); + +#endif /* PMS_MATCH_H */ 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; +} diff --git a/src/pms_ref.h b/src/pms_ref.h new file mode 100644 index 0000000..d0da38b --- /dev/null +++ b/src/pms_ref.h @@ -0,0 +1,37 @@ +#ifndef PMS_REF_H +#define PMS_REF_H + +#include <regex.h> +#include <stdbool.h> + +#define PMS_REF_SEARCH 1 +#define PMS_REF_EXACT 2 +#define PMS_REF_EXACT_SET 3 +#define PMS_REF_RANGE 4 +#define PMS_REF_RANGE_EXT 5 + +#define PMS_MAX_VERSE_SET 64 + +typedef struct pms_ref { + int type; + unsigned int book; + unsigned int chapter; + unsigned int chapter_end; + unsigned int verse; + unsigned int verse_end; + unsigned int verse_set[PMS_MAX_VERSE_SET]; + int verse_set_count; + char *search_str; + regex_t search; +} pms_ref; + +pms_ref *pms_newref(void); +void pms_freeref(pms_ref *ref); + +/* Match a book name, abbreviation, or modern alias to book number (1..73), or 0 if not found. */ +int pms_book_from_name(const char *name); + +/* Parse a reference string (e.g. "Gn 1:1-5", "1Rs:3:16", "/principio", "Jo 1 /luz") into ref. Returns 0 on success. */ +int pms_parse_ref(const char *input, pms_ref *ref); + +#endif /* PMS_REF_H */ diff --git a/src/pms_render.c b/src/pms_render.c new file mode 100644 index 0000000..d433ced --- /dev/null +++ b/src/pms_render.c @@ -0,0 +1,306 @@ +#include "pms_render.h" +#include "pms_data.h" +#include "pms_match.h" +#include "pms_util.h" +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#define ESC_VERSE_START "\x1b[32m" +#define ESC_VERSE_END "\x1b[0m" +#define ESC_BOLD_START "\x1b[1m" +#define ESC_BOLD_END "\x1b[0m" +#define ESC_SEARCH_START "\x1b[7m" +#define ESC_SEARCH_END "\x1b[27m" +#define ESC_NOTE_START "\x1b[36m" +#define ESC_NOTE_END "\x1b[0m" + +static const char *get_book_name(int book_num) { + for (int i = 0; i < pms_books_length; i++) { + if (pms_books[i].number == book_num) { + return pms_books[i].name; + } + } + return ""; +} + +/* + * Build a version of `text` with ANSI inverse-video highlights around regex matches + * when ref->type == PMS_REF_SEARCH and config->highlighting is true. + * Caller must NOT free the returned pointer if it equals `text`; we always return a newly + * allocated string that the caller frees. + */ +static char *highlight_search_matches(const char *text, const pms_ref *ref, const pms_config *config) { + if (!config->highlighting || ref->type != PMS_REF_SEARCH || !ref->search_str) { + return strdup(text); + } + + size_t text_len = strlen(text); + char *folded = malloc(text_len + 2); + int *byte_map = malloc((text_len + 2) * sizeof(int)); + if (!folded || !byte_map) { + free(folded); + free(byte_map); + return strdup(text); + } + + size_t folded_len = pms_fold_utf8(text, folded, text_len + 2, byte_map); + size_t cap = text_len * 2 + 128; + char *out = malloc(cap); + if (!out) { + free(folded); + free(byte_map); + return strdup(text); + } + + size_t out_pos = 0; + size_t f_pos = 0; + size_t orig_pos = 0; + regmatch_t m[1]; + + while (f_pos < folded_len && regexec(&ref->search, folded + f_pos, 1, m, f_pos > 0 ? REG_NOTBOL : 0) == 0) { + if (m[0].rm_so == m[0].rm_eo) { + break; + } + size_t match_f_start = f_pos + (size_t)m[0].rm_so; + size_t match_f_end = f_pos + (size_t)m[0].rm_eo; + size_t match_orig_start = (size_t)byte_map[match_f_start]; + size_t match_orig_end = (size_t)byte_map[match_f_end]; + + size_t needed = (match_orig_end - orig_pos) + 32; + if (out_pos + needed >= cap) { + cap = (cap + needed) * 2; + char * resized = realloc(out, cap); + if (!resized) break; + out = resized; + } + + if (match_orig_start > orig_pos) { + size_t pre = match_orig_start - orig_pos; + memcpy(out + out_pos, text + orig_pos, pre); + out_pos += pre; + } + memcpy(out + out_pos, ESC_SEARCH_START, strlen(ESC_SEARCH_START)); + out_pos += strlen(ESC_SEARCH_START); + + if (match_orig_end > match_orig_start) { + size_t mlen = match_orig_end - match_orig_start; + memcpy(out + out_pos, text + match_orig_start, mlen); + out_pos += mlen; + } + memcpy(out + out_pos, ESC_SEARCH_END, strlen(ESC_SEARCH_END)); + out_pos += strlen(ESC_SEARCH_END); + + orig_pos = match_orig_end; + f_pos = match_f_end; + } + + if (orig_pos < text_len) { + size_t rem = text_len - orig_pos; + if (out_pos + rem + 2 >= cap) { + cap = out_pos + rem + 16; + char *resized = realloc(out, cap); + if (resized) out = resized; + } + memcpy(out + out_pos, text + orig_pos, rem); + out_pos += rem; + } + out[out_pos] = '\0'; + + free(folded); + free(byte_map); + return out; +} + +/* Compute visible display column count of a token (ignoring ANSI SGR sequences \x1b[...m). */ +static int visible_utf8_cols(const char *s, size_t len) { + int cols = 0; + size_t i = 0; + while (i < len && s[i] != '\0') { + if (s[i] == '\x1b' && i + 1 < len && s[i + 1] == '[') { + i += 2; + while (i < len && s[i] != 'm' && s[i] != '\0') { + i++; + } + if (i < len && s[i] == 'm') { + i++; + } + continue; + } + if (((unsigned char)s[i] & 0xC0) != 0x80) { + cols++; + } + i++; + } + return cols; +} + +static void print_wrapped(FILE *out, const char *text, int start_col, int indent_col, int max_cols) { + if (max_cols <= 0) { + fputs(text, out); + fputc('\n', out); + return; + } + + int cur_col = start_col; + const char *p = text; + bool first_word = true; + + while (*p) { + while (*p == ' ') p++; + if (*p == '\0') break; + const char *w_end = p; + while (*w_end && *w_end != ' ') w_end++; + size_t w_bytes = (size_t)(w_end - p); + int w_cols = visible_utf8_cols(p, w_bytes); + + if (!first_word && cur_col + 1 + w_cols > max_cols && cur_col > indent_col) { + fputc('\n', out); + for (int i = 0; i < indent_col; i++) { + fputc(' ', out); + } + cur_col = indent_col; + fwrite(p, 1, w_bytes, out); + cur_col += w_cols; + } else { + if (!first_word) { + fputc(' ', out); + cur_col += 1; + } + fwrite(p, 1, w_bytes, out); + cur_col += w_cols; + } + first_word = false; + p = w_end; + } + fputc('\n', out); +} + +static void render_verse_notes(const pms_verse *v, int next_book, int next_chapter, int next_verse, + const pms_config *config, FILE *out, bool *printed_notes) { + if (!config->show_notes) { + return; + } + const char *bname = get_book_name(v->book); + for (int k = 0; k < pms_notes_length; k++) { + if (printed_notes[k]) { + continue; + } + const pms_note *n = &pms_notes[k]; + if (n->book != v->book || n->chapter != v->chapter) { + continue; + } + if (v->verse < n->verse_start || v->verse > n->verse_end) { + continue; + } + /* If note spans multiple verses (e.g. 5-7) and the next rendered verse is also in the span, + wait until the last rendered verse of the span so the note appears right after those verses. */ + if (next_book == v->book && next_chapter == v->chapter && + next_verse >= n->verse_start && next_verse <= n->verse_end) { + continue; + } + + printed_notes[k] = true; + if (config->pretty) { + char prefix[128]; + int p_cols = snprintf(prefix, sizeof(prefix), " [Nota v. %s] ", n->label); + if (config->highlighting) { + fprintf(out, " %s[Nota v. %s]%s ", ESC_NOTE_START, n->label, ESC_NOTE_END); + } else { + fputs(prefix, out); + } + print_wrapped(out, n->text, p_cols, 8, config->maximum_line_length); + } else { + if (config->highlighting) { + fprintf(out, "%s %d:%s (Nota)\t", bname, v->chapter, n->label); + } else { + fprintf(out, "%s %d:%s (Nota)\t", bname, v->chapter, n->label); + } + print_wrapped(out, n->text, 0, 0, config->maximum_line_length); + } + } +} + +int pms_render(const pms_ref *ref, const pms_config *config, FILE *out) { + pms_match_data match_data = {0}; + int prev_book = -1; + int prev_chapter = -1; + int prev_verse = -1; + int rendered_count = 0; + + bool *printed_notes = NULL; + if (config->show_notes && pms_notes_length > 0) { + printed_notes = calloc((size_t)pms_notes_length, sizeof(bool)); + } + + int idx = pms_next_verse(ref, config, &match_data); + while (idx != -1) { + const pms_verse *v = &pms_verses[idx]; + int next_idx = pms_next_verse(ref, config, &match_data); + int next_book = (next_idx != -1) ? pms_verses[next_idx].book : -1; + int next_chap = (next_idx != -1) ? pms_verses[next_idx].chapter : -1; + int next_vnum = (next_idx != -1) ? pms_verses[next_idx].verse : -1; + + char *v_text = highlight_search_matches(v->text, ref, config); + const char *bname = get_book_name(v->book); + + if (config->pretty) { + if (v->book != prev_book || v->chapter != prev_chapter) { + if (rendered_count > 0) { + fputc('\n', out); + } + const char *ch_label = (v->book == 21) ? "Salmo" : ""; + if (config->highlighting) { + if (ch_label[0]) { + fprintf(out, "%s%s — %s %d%s\n\n", ESC_BOLD_START, bname, ch_label, v->chapter, ESC_BOLD_END); + } else { + fprintf(out, "%s%s %d%s\n\n", ESC_BOLD_START, bname, v->chapter, ESC_BOLD_END); + } + } else { + if (ch_label[0]) { + fprintf(out, "%s — %s %d\n\n", bname, ch_label, v->chapter); + } else { + fprintf(out, "%s %d\n\n", bname, v->chapter); + } + } + } else if (prev_verse != -1 && v->verse > prev_verse + 1) { + fprintf(out, " ...\n"); + } + + char vprefix[64]; + int p_cols = snprintf(vprefix, sizeof(vprefix), " %d:%d ", v->chapter, v->verse); + if (config->highlighting) { + fprintf(out, " %s%d:%d%s ", ESC_VERSE_START, v->chapter, v->verse, ESC_VERSE_END); + } else { + fputs(vprefix, out); + } + print_wrapped(out, v_text, p_cols, 6, config->maximum_line_length); + } else { + char vprefix[128]; + snprintf(vprefix, sizeof(vprefix), "%s %d:%d\t", bname, v->chapter, v->verse); + int p_cols = (int)pms_utf8_char_len(vprefix, strlen(vprefix)) + 4; + if (config->highlighting) { + fprintf(out, "%s%s %d:%d%s\t", ESC_VERSE_START, bname, v->chapter, v->verse, ESC_VERSE_END); + } else { + fputs(vprefix, out); + } + print_wrapped(out, v_text, p_cols, 0, config->maximum_line_length); + } + + free(v_text); + + if (printed_notes) { + render_verse_notes(v, next_book, next_chap, next_vnum, config, out, printed_notes); + } + + prev_book = v->book; + prev_chapter = v->chapter; + prev_verse = v->verse; + rendered_count++; + idx = next_idx; + } + + free(printed_notes); + return rendered_count; +} diff --git a/src/pms_render.h b/src/pms_render.h new file mode 100644 index 0000000..0639488 --- /dev/null +++ b/src/pms_render.h @@ -0,0 +1,10 @@ +#ifndef PMS_RENDER_H +#define PMS_RENDER_H + +#include "pms_config.h" +#include "pms_ref.h" +#include <stdio.h> + +int pms_render(const pms_ref *ref, const pms_config *config, FILE *out); + +#endif /* PMS_RENDER_H */ diff --git a/src/pms_util.c b/src/pms_util.c new file mode 100644 index 0000000..635fa3c --- /dev/null +++ b/src/pms_util.c @@ -0,0 +1,89 @@ +#include "pms_util.h" +#include <ctype.h> +#include <string.h> + +static char fold_latin1_c3(unsigned char b2) { + if ((b2 >= 0x80 && b2 <= 0x85) || (b2 >= 0xA0 && b2 <= 0xA5)) return 'a'; + if (b2 == 0x87 || b2 == 0xA7) return 'c'; + if ((b2 >= 0x88 && b2 <= 0x8B) || (b2 >= 0xA8 && b2 <= 0xAB)) return 'e'; + if ((b2 >= 0x8C && b2 <= 0x8F) || (b2 >= 0xAC && b2 <= 0xAF)) return 'i'; + if (b2 == 0x91 || b2 == 0xB1) return 'n'; + if ((b2 >= 0x92 && b2 <= 0x96) || (b2 >= 0xB2 && b2 <= 0xB6)) return 'o'; + if ((b2 >= 0x99 && b2 <= 0x9C) || (b2 >= 0xB9 && b2 <= 0xBC)) return 'u'; + if (b2 == 0x9D || b2 == 0xBD || b2 == 0xBF) return 'y'; + return 0; +} + +size_t pms_fold_utf8(const char *src, char *dst, size_t dst_size, int *byte_map) { + if (!src || !dst || dst_size == 0) { + return 0; + } + size_t i = 0; + size_t out = 0; + while (src[i] != '\0' && out + 1 < dst_size) { + unsigned char c = (unsigned char)src[i]; + if (c < 0x80) { + if (byte_map) byte_map[out] = (int)i; + dst[out++] = (char)tolower(c); + i += 1; + } else if (c == 0xC3 && src[i + 1] != '\0') { + unsigned char b2 = (unsigned char)src[i + 1]; + char folded = fold_latin1_c3(b2); + if (byte_map) byte_map[out] = (int)i; + dst[out++] = folded ? folded : '?'; + i += 2; + } else if ((c & 0xE0) == 0xC0 && src[i + 1] != '\0') { + if (byte_map) byte_map[out] = (int)i; + dst[out++] = ' '; + i += 2; + } else if ((c & 0xF0) == 0xE0 && src[i + 1] != '\0' && src[i + 2] != '\0') { + /* Handle curly apostrophes U+2018 / U+2019 (E2 80 98 / E2 80 99) and dashes */ + unsigned char b2 = (unsigned char)src[i + 1]; + unsigned char b3 = (unsigned char)src[i + 2]; + char repl = ' '; + if (c == 0xE2 && b2 == 0x80 && (b3 == 0x98 || b3 == 0x99)) { + repl = '\''; + } else if (c == 0xE2 && b2 == 0x80 && (b3 == 0x93 || b3 == 0x94)) { + repl = '-'; + } + if (byte_map) byte_map[out] = (int)i; + dst[out++] = repl; + i += 3; + } else if ((c & 0xF8) == 0xF0 && src[i + 1] != '\0' && src[i + 2] != '\0' && src[i + 3] != '\0') { + if (byte_map) byte_map[out] = (int)i; + dst[out++] = ' '; + i += 4; + } else { + i += 1; + } + } + if (byte_map) { + byte_map[out] = (int)strlen(src); + } + dst[out] = '\0'; + return out; +} + +size_t pms_normalize_book_key(const char *src, char *dst, size_t dst_size) { + char folded[256]; + pms_fold_utf8(src, folded, sizeof(folded), NULL); + size_t out = 0; + for (size_t i = 0; folded[i] != '\0' && out + 1 < dst_size; i++) { + unsigned char c = (unsigned char)folded[i]; + if (isalnum(c)) { + dst[out++] = (char)c; + } + } + dst[out] = '\0'; + return out; +} + +size_t pms_utf8_char_len(const char *s, size_t byte_len) { + size_t count = 0; + for (size_t i = 0; i < byte_len && s[i] != '\0'; i++) { + if (((unsigned char)s[i] & 0xC0) != 0x80) { + count++; + } + } + return count; +} diff --git a/src/pms_util.h b/src/pms_util.h new file mode 100644 index 0000000..03ae8ba --- /dev/null +++ b/src/pms_util.h @@ -0,0 +1,27 @@ +#ifndef PMS_UTIL_H +#define PMS_UTIL_H + +#include <stddef.h> + +/* + * Fold a UTF-8 string to lowercase ASCII (stripping Portuguese diacritics). + * Writes at most dst_size - 1 characters + NUL to dst. + * If byte_map is non-NULL (must have at least dst_size ints), byte_map[i] + * stores the byte offset in `src` corresponding to `dst[i]`, and + * byte_map[folded_len] stores strlen(src). + * Returns the length of the folded string in `dst`. + */ +size_t pms_fold_utf8(const char *src, char *dst, size_t dst_size, int *byte_map); + +/* + * Normalize a book name/abbreviation query into lowercase alphanumeric ASCII + * (stripping spaces, dots, hyphens, parentheses, and accents). + */ +size_t pms_normalize_book_key(const char *src, char *dst, size_t dst_size); + +/* + * Count display columns (UTF-8 codepoints) in a NUL-terminated string or slice. + */ +size_t pms_utf8_char_len(const char *s, size_t byte_len); + +#endif /* PMS_UTIL_H */ |
