aboutsummaryrefslogtreecommitdiff
path: root/src/pms_render.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_render.c
downloadpms-master.tar.gz
pms-master.zip
Initial commit: Padre Matos Soares (1932) Bible corpus and pms CLIHEADmaster
Diffstat (limited to 'src/pms_render.c')
-rw-r--r--src/pms_render.c306
1 files changed, 306 insertions, 0 deletions
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;
+}