#include "pms_render.h" #include "pms_data.h" #include "pms_match.h" #include "pms_util.h" #include #include #include #include #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; }