diff options
| author | Miguel Dalberto Pedro <miguel.pedro@precisioninno.com> | 2026-10-29 17:55:36 -0300 |
|---|---|---|
| committer | Miguel Dalberto Pedro <miguel.pedro@precisioninno.com> | 2026-10-29 17:55:36 -0300 |
| commit | 915478aef931cb57fe56fe568ab4982454a29812 (patch) | |
| tree | 33057f4773852d99e8cd7ad92d568e79f34847ee /src/pms_render.c | |
| download | pms-915478aef931cb57fe56fe568ab4982454a29812.tar.gz pms-915478aef931cb57fe56fe568ab4982454a29812.zip | |
Diffstat (limited to 'src/pms_render.c')
| -rw-r--r-- | src/pms_render.c | 306 |
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; +} |
