generate book data, lookup books when parsing reference
[ssp.git] / src / kjv.c
blob4dd6314ca720e273f176b9e17897dbfcee626023
1 /*
2 kjv: Read the Word of God from your terminal
4 License: Public domain
5 */
7 #include <assert.h>
8 #include <stdio.h>
9 #include <stdlib.h>
10 #include <unistd.h>
11 #include <stdbool.h>
12 #include <string.h>
13 #include <signal.h>
14 #include <readline/readline.h>
15 #include <readline/history.h>
16 #include <regex.h>
17 #include <sys/types.h>
18 #include <sys/wait.h>
19 #include <ctype.h>
20 #include <sys/ioctl.h>
22 #include "data.h"
23 #include "intset.h"
25 typedef struct {
26 int maximum_line_length;
28 int context_before;
29 int context_after;
30 bool context_chapter;
31 } kjv_config;
33 #define KJV_REF_SEARCH 1
34 #define KJV_REF_EXACT 2
35 #define KJV_REF_EXACT_SET 3
36 #define KJV_REF_RANGE 4
37 #define KJV_REF_RANGE_EXT 5
39 typedef struct kjv_ref {
40 int type;
41 unsigned int book;
42 unsigned int chapter;
43 unsigned int chapter_end;
44 unsigned int verse;
45 unsigned int verse_end;
46 intset *verse_set;
47 char *search_str;
48 regex_t search;
49 } kjv_ref;
51 static kjv_ref *
52 kjv_newref()
54 return calloc(1, sizeof(kjv_ref));
57 static void
58 kjv_freeref(kjv_ref *ref)
60 if (ref) {
61 free(ref->search_str);
62 regfree(&ref->search);
63 free(ref);
68 static bool
69 kjv_bookequal(const char *a, const char *b, bool short_match)
71 for (size_t i = 0, j = 0; ; ) {
72 if ((!a[i] && !b[j]) || (short_match && !b[j])) {
73 return true;
74 } else if (a[i] == ' ') {
75 i++;
76 } else if (b[j] == ' ') {
77 j++;
78 } else if (tolower(a[i]) != tolower(b[j])) {
79 return false;
80 } else {
81 i++;
82 j++;
87 static bool
88 kjv_book_matches(const kjv_book *book, const char *s)
90 return kjv_bookequal(book->name, s, false) ||
91 kjv_bookequal(book->abbr, s, false) ||
92 kjv_bookequal(book->name, s, true);
95 static int
96 kjv_book_fromname(const char *s)
98 for (int i = 0; i < kjv_books_length; i++) {
99 const kjv_book *book = &kjv_books[i];
100 if (kjv_book_matches(book, s)) {
101 return book->number;
104 return 0;
107 static int
108 kjv_scanbook(const char *s, int *n)
110 int i;
111 int mode = 0;
112 for (i = 0; s[i]; i++) {
113 if (s[i] == ' ') {
114 continue;
115 } else if (('a' <= s[i] && s[i] <= 'z') || ('A' <= s[i] && s[i] <= 'Z')) {
116 mode = 1;
117 } else if ('0' <= s[i] && s[i] <= '9' && mode == 0) {
118 continue;
119 } else {
120 break;
123 *n = i;
124 return mode >= 1;
127 static int
128 kjv_parseref(kjv_ref *ref, const char *ref_str)
130 // 1. <book>
131 // 2. <book>:?<chapter>
132 // 3. <book>:?<chapter>:<verse>
133 // 3a. <book>:?<chapter>:<verse>[,<verse>]...
134 // 4. <book>:?<chapter>-<chapter>
135 // 5. <book>:?<chapter>:<verse>-<verse>
136 // 6. <book>:?<chapter>:<verse>-<chapter>:<verse>
137 // 7. /<search>
138 // 8. <book>/search
139 // 9. <book>:?<chapter>/search
141 ref->type = 0;
142 ref->book = 0;
143 ref->chapter = 0;
144 ref->chapter_end = 0;
145 ref->verse = 0;
146 ref->verse_end = 0;
147 intset_free(ref->verse_set);
148 ref->verse_set = NULL;
149 free(ref->search_str);
150 ref->search_str = NULL;
151 regfree(&ref->search);
153 int n = 0;
154 if (kjv_scanbook(ref_str, &n) == 1) {
155 // 1, 2, 3, 3a, 4, 5, 6, 8, 9
156 char *bookname = strndup(ref_str, n);
157 ref->book = kjv_book_fromname(bookname);
158 free(bookname);
159 ref_str = &ref_str[n];
160 } else if (ref_str[0] == '/') {
161 // 7
162 goto search;
163 } else {
164 return 1;
167 if (sscanf(ref_str, ":%u%n", &ref->chapter, &n) == 1 || sscanf(ref_str, "%u%n", &ref->chapter, &n) == 1) {
168 // 2, 3, 3a, 4, 5, 6, 9
169 ref_str = &ref_str[n];
170 } else if (ref_str[0] == '/') {
171 // 8
172 goto search;
173 } else if (ref_str[0] == '\0') {
174 // 1
175 ref->type = KJV_REF_EXACT;
176 return 0;
177 } else {
178 return 1;
181 if (sscanf(ref_str, ":%u%n", &ref->verse, &n) == 1) {
182 // 3, 3a, 5, 6
183 ref_str = &ref_str[n];
184 } else if (sscanf(ref_str, "-%u%n", &ref->chapter_end, &n) == 1) {
185 // 4
186 if (ref_str[n] != '\0') {
187 return 1;
189 ref->type = KJV_REF_RANGE;
190 return 0;
191 } else if (ref_str[0] == '/') {
192 // 9
193 goto search;
194 } else if (ref_str[0] == '\0') {
195 // 2
196 ref->type = KJV_REF_EXACT;
197 return 0;
198 } else {
199 return 1;
202 unsigned int value;
203 int ret = sscanf(ref_str, "-%u%n", &value, &n);
204 if (ret == 1 && ref_str[n] == '\0') {
205 // 5
206 ref->verse_end = value;
207 ref->type = KJV_REF_RANGE;
208 return 0;
209 } else if (ret == 1) {
210 // 6
211 ref->chapter_end = value;
212 ref_str = &ref_str[n];
213 } else if (ref_str[0] == '\0') {
214 // 3
215 ref->type = KJV_REF_EXACT;
216 return 0;
217 } else if (sscanf(ref_str, ",%u%n", &value, &n) == 1) {
218 // 3a
219 ref->verse_set = intset_new();
220 intset_add(ref->verse_set, ref->verse);
221 intset_add(ref->verse_set, value);
222 ref_str = &ref_str[n];
223 while (true) {
224 if (sscanf(ref_str, ",%u%n", &value, &n) != 1) {
225 break;
227 intset_add(ref->verse_set, value);
228 ref_str = &ref_str[n];
230 if (ref_str[0] != '\0') {
231 return 1;
233 ref->type = KJV_REF_EXACT_SET;
234 return 0;
235 } else {
236 return 1;
239 if (sscanf(ref_str, ":%u%n", &ref->verse_end, &n) == 1 && ref_str[n] == '\0') {
240 // 6
241 ref->type = KJV_REF_RANGE_EXT;
242 return 0;
243 } else {
244 return 1;
247 search:
248 ref->type = KJV_REF_SEARCH;
249 if (regcomp(&ref->search, &ref_str[1], REG_EXTENDED|REG_ICASE|REG_NOSUB) != 0) {
250 return 2;
252 ref->search_str = strdup(&ref_str[1]);
253 return 0;
256 static char *
257 str_join(size_t n, char *strs[])
259 size_t length = 0;
260 for (size_t i = 0; i < n; i++) {
261 if (i > 0) {
262 length++;
264 length += strlen(strs[i]);
266 char *str = malloc(length + 1);
267 str[0] = '\0';
268 for (size_t i = 0; i < n; i++) {
269 if (i > 0) {
270 strcat(str, " ");
272 strcat(str, strs[i]);
274 return str;
277 static bool
278 kjv_verse_matches(const kjv_ref *ref, const kjv_verse *verse)
280 switch (ref->type) {
281 case KJV_REF_SEARCH:
282 return (ref->book == 0 || ref->book == verse->book) &&
283 (ref->chapter == 0 || verse->chapter == ref->chapter) &&
284 regexec(&ref->search, verse->text, 0, NULL, 0) == 0;
286 case KJV_REF_EXACT:
287 return ref->book == verse->book &&
288 (ref->chapter == 0 || ref->chapter == verse->chapter) &&
289 (ref->verse == 0 || ref->verse == verse->verse);
291 case KJV_REF_EXACT_SET:
292 return ref->book == verse->book &&
293 (ref->chapter == 0 || verse->chapter == ref->chapter) &&
294 intset_contains(ref->verse_set, verse->verse);
296 case KJV_REF_RANGE:
297 return ref->book == verse->book &&
298 ((ref->chapter_end == 0 && ref->chapter == verse->chapter) ||
299 (verse->chapter >= ref->chapter && verse->chapter <= ref->chapter_end)) &&
300 (ref->verse == 0 || verse->verse >= ref->verse) &&
301 (ref->verse_end == 0 || verse->verse <= ref->verse_end);
303 case KJV_REF_RANGE_EXT:
304 return ref->book == verse->book &&
306 (verse->chapter == ref->chapter && verse->verse >= ref->verse && ref->chapter != ref->chapter_end) ||
307 (verse->chapter > ref->chapter && verse->chapter < ref->chapter_end) ||
308 (verse->chapter == ref->chapter_end && verse->verse <= ref->verse_end && ref->chapter != ref->chapter_end) ||
309 (ref->chapter == ref->chapter_end && verse->chapter == ref->chapter && verse->verse >= ref->verse && verse->verse <= ref->verse_end)
312 default:
313 return false;
317 #define KJV_DIRECTION_BEFORE -1
318 #define KJV_DIRECTION_AFTER 1
320 static int
321 kjv_chapter_bounds(int i, int direction, int maximum_steps)
323 assert(direction == -1 || direction == 1);
325 int steps = 0;
326 for ( ; 0 <= i && i < kjv_verses_length; i += direction) {
327 if (maximum_steps != -1 && steps >= maximum_steps) {
328 break;
330 if ((direction == -1 && i == 0) || (direction == 1 && i + 1 == kjv_verses_length)) {
331 break;
333 const kjv_verse *current = &kjv_verses[i], *next = &kjv_verses[i + direction];
334 if (current->book != next->book || current->chapter != next->chapter) {
335 break;
337 steps++;
339 return i;
342 static int
343 kjv_next_match(const kjv_ref *ref, int i)
345 for ( ; i < kjv_verses_length; i++) {
346 const kjv_verse *verse = &kjv_verses[i];
347 if (kjv_verse_matches(ref, verse)) {
348 return i;
351 return -1;
354 typedef struct {
355 int start;
356 int end;
357 } kjv_range;
359 typedef struct {
360 int current;
361 int next_match;
362 kjv_range matches[2];
363 } kjv_next_data;
365 static void
366 kjv_next_addrange(kjv_next_data *next, kjv_range range) {
367 if (next->matches[0].start == -1 && next->matches[0].end == -1) {
368 next->matches[0] = range;
369 } else if (range.start < next->matches[0].end) {
370 next->matches[0] = range;
371 } else {
372 next->matches[1] = range;
376 static int
377 kjv_next_verse(const kjv_ref *ref, const kjv_config *config, kjv_next_data *next)
379 if (next->current >= kjv_verses_length) {
380 return -1;
383 if (next->matches[0].start != -1 && next->matches[0].end != -1 && next->current >= next->matches[0].end) {
384 next->matches[0] = next->matches[1];
385 next->matches[1] = (kjv_range){-1, -1};
388 if ((next->next_match == -1 || next->next_match < next->current) && next->next_match < kjv_verses_length) {
389 int next_match = kjv_next_match(ref, next->current);
390 if (next_match >= 0) {
391 next->next_match = next_match;
392 kjv_range bounds = {
393 .start = kjv_chapter_bounds(next_match, KJV_DIRECTION_BEFORE, config->context_chapter ? -1 : config->context_before),
394 .end = kjv_chapter_bounds(next_match, KJV_DIRECTION_AFTER, config->context_chapter ? -1 : config->context_after) + 1,
396 kjv_next_addrange(next, bounds);
397 } else {
398 next_match = kjv_verses_length;
402 if (next->matches[0].start == -1 && next->matches[0].end == -1) {
403 return -1;
406 if (next->current < next->matches[0].start) {
407 next->current = next->matches[0].start;
410 return next->current++;
413 #define ESC_BOLD "\033[1m"
414 #define ESC_UNDERLINE "\033[4m"
415 #define ESC_RESET "\033[m"
417 static void
418 kjv_output_verse(const kjv_verse *verse, FILE *f, const kjv_config *config)
420 fprintf(f, ESC_BOLD "%d:%d" ESC_RESET "\t", verse->chapter, verse->verse);
421 char verse_text[1024];
422 strcpy(verse_text, verse->text);
423 size_t characters_printed = 0;
424 char *word = strtok(verse_text, " ");
425 while (word != NULL) {
426 size_t word_length = strlen(word);
427 if (characters_printed + word_length + (characters_printed > 0 ? 1 : 0) > config->maximum_line_length - 8 - 2) {
428 fprintf(f, "\n\t");
429 characters_printed = 0;
431 if (characters_printed > 0) {
432 fprintf(f, " ");
433 characters_printed++;
435 fprintf(f, "%s", word);
436 characters_printed += word_length;
437 word = strtok(NULL, " ");
439 fprintf(f, "\n");
442 static bool
443 kjv_output(const kjv_ref *ref, FILE *f, const kjv_config *config)
445 kjv_next_data next = {
446 .current = 0,
447 .next_match = -1,
448 .matches = {
449 {-1, -1},
450 {-1, -1},
454 kjv_verse *last_printed = NULL;
455 for (int verse_id; (verse_id = kjv_next_verse(ref, config, &next)) != -1; ) {
456 kjv_verse *verse = &kjv_verses[verse_id];
457 if (last_printed == NULL || verse->book != last_printed->book) {
458 if (last_printed != NULL) {
459 fprintf(f, "\n");
461 fprintf(f, ESC_UNDERLINE "%s" ESC_RESET "\n\n", kjv_books[verse->book - 1].name);
463 kjv_output_verse(verse, f, config);
464 last_printed = verse;
466 return last_printed != NULL;
469 static int
470 kjv_render(const kjv_ref *ref, const kjv_config *config)
472 int fds[2];
473 if (pipe(fds) == -1) {
474 return 1;
477 pid_t pid = fork();
478 if (pid == 0) {
479 close(fds[1]);
480 dup2(fds[0], STDIN_FILENO);
481 char *args[9] = {NULL};
482 int arg = 0;
483 args[arg++] = "less";
484 args[arg++] = "-J";
485 args[arg++] = "-I";
486 if (ref->search_str != NULL) {
487 args[arg++] = "-p";
488 args[arg++] = ref->search_str;
490 args[arg++] = "-R";
491 args[arg++] = "-f";
492 args[arg++] = "-";
493 args[arg++] = NULL;
494 execvp("less", args);
495 printf("unable not exec less\n");
496 _exit(0);
497 } else if (pid == -1) {
498 printf("unable to fork\n");
499 return 2;
501 close(fds[0]);
502 FILE *output = fdopen(fds[1], "w");
503 bool printed = kjv_output(ref, output, config);
504 if (!printed) {
505 kill(pid, SIGTERM);
507 fclose(output);
508 waitpid(pid, NULL, 0);
509 if (!printed) {
510 printf("unknown reference\n");
512 return 0;
515 const char *
516 usage = "usage: kjv [flags] [reference...]\n"
517 "\n"
518 "Flags:\n"
519 " -A num number of verses of context after matching verses\n"
520 " -B num number of verses of context before matching verses\n"
521 " -C show matching verses in context of the chapter\n"
522 " -l list books\n"
523 " -h show help\n"
524 "\n"
525 "Reference:\n"
526 " <Book>\n"
527 " Individual book\n"
528 " <Book>:<Chapter>\n"
529 " Individual chapter of a book\n"
530 " <Book>:<Chapter>:<Verse>[,<Verse>]...\n"
531 " Individual verse(s) of a specific chapter of a book\n"
532 " <Book>:<Chapter>-<Chapter>\n"
533 " Range of chapters in a book\n"
534 " <Book>:<Chapter>:<Verse>-<Verse>\n"
535 " Range of verses in a book chapter\n"
536 " <Book>:<Chapter>:<Verse>-<Chapter>:<Verse>\n"
537 " Range of chapters and verses in a book\n"
538 "\n"
539 " /<Search>\n"
540 " All verses that match a pattern\n"
541 " <Book>/<Search>\n"
542 " All verses in a book that match a pattern\n"
543 " <Book>:<Chapter>/<Search>\n"
544 " All verses in a chapter of a book that match a pattern\n";
547 main(int argc, char *argv[])
549 kjv_config config = {
550 .maximum_line_length = 80,
552 .context_before = 0,
553 .context_after = 0,
554 .context_chapter = false,
557 bool list_books = false;
559 opterr = 0;
560 for (int opt; (opt = getopt(argc, argv, "A:B:ClWh")) != -1; ) {
561 char *endptr;
562 switch (opt) {
563 case 'A':
564 config.context_after = strtol(optarg, &endptr, 10);
565 if (endptr[0] != '\0') {
566 fprintf(stderr, "kjv: invalid flag value for -A\n\n%s", usage);
567 return 1;
569 break;
570 case 'B':
571 config.context_before = strtol(optarg, &endptr, 10);
572 if (endptr[0] != '\0') {
573 fprintf(stderr, "kjv: invalid flag value for -B\n\n%s", usage);
574 return 1;
576 break;
577 case 'C':
578 config.context_chapter = true;
579 break;
580 case 'l':
581 list_books = true;
582 break;
583 case 'h':
584 printf("%s", usage);
585 return 0;
586 case '?':
587 fprintf(stderr, "kjv: invalid flag -%c\n\n%s", optopt, usage);
588 return 1;
592 if (list_books) {
593 for (int i = 0; i < kjv_books_length; i++) {
594 kjv_book *book = &kjv_books[i];
595 printf("%s (%s)\n", book->name, book->abbr);
597 return 0;
600 struct winsize ttysize;
601 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ttysize) == 0 && ttysize.ws_col > 0) {
602 config.maximum_line_length = ttysize.ws_col;
605 signal(SIGPIPE, SIG_IGN);
607 if (argc == optind) {
608 using_history();
609 while (true) {
610 char *input = readline("kjv> ");
611 if (input == NULL) {
612 break;
614 add_history(input);
615 kjv_ref *ref = kjv_newref();
616 int success = kjv_parseref(ref, input);
617 free(input);
618 if (success == 0) {
619 kjv_render(ref, &config);
621 kjv_freeref(ref);
623 } else {
624 char *ref_str = str_join(argc-optind, &argv[optind]);
625 kjv_ref *ref = kjv_newref();
626 int success = kjv_parseref(ref, ref_str);
627 free(ref_str);
628 if (success == 0) {
629 kjv_render(ref, &config);
631 kjv_freeref(ref);
634 return 0;