/* mktext -- resolve a lectionary's verse keys against a scripture corpus and * write the LZSS-packed text clectio compiles in. This is what makes clectio * bring-your-own-Bible: point it at any corpus formatted like ours (a 6-column * TSV: Book, Abbrev, BookNum, Chapter, Verse, Text) and it produces the text for * exactly the verses the calendar cites -- nothing more. The corpus must use the * same (Vulgate) chapter/verse numbering as the keys. * * usage: mktext verses.keys corpus.tsv out.lz * out.lz = [uint32 LE rawlen][LZSS-packed "chap:verse text\n" lines]. */ #include #include #include typedef struct { char *key; /* "Book\tChap\tVerse" */ char *cv; /* "Chap:Verse" */ char *text; /* verse text */ } Rec; static Rec *recs; static size_t nrec; static int cmp(const void *a, const void *b) { return strcmp(((const Rec *)a)->key, ((const Rec *)b)->key); } static char *slurp(const char *path, size_t *len) { FILE *f = fopen(path, "rb"); if (!f) { fprintf(stderr, "mktext: cannot open %s\n", path); exit(1); } fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET); char *buf = malloc((size_t)n + 1); if (fread(buf, 1, (size_t)n, f) != (size_t)n) { fprintf(stderr, "mktext: read error\n"); exit(1); } buf[n] = 0; fclose(f); *len = (size_t)n; return buf; } /* split a TSV line into fields in place; return field count (max 6). */ static int fields(char *line, char *f[6]) { int n = 0; f[n++] = line; for (char *p = line; *p && n < 6; p++) if (*p == '\t') { *p = 0; f[n++] = p + 1; } return n; } int main(int argc, char **argv) { if (argc != 4) { fprintf(stderr, "usage: mktext verses.keys corpus.tsv out.lz\n"); return 2; } size_t klen, tlen; char *keys = slurp(argv[1], &klen); char *tsv = slurp(argv[2], &tlen); /* index the corpus by (Book\tChap\tVerse) */ size_t cap = 4096; recs = malloc(cap * sizeof(Rec)); for (char *line = strtok(tsv, "\n"); line; line = strtok(NULL, "\n")) { char *fl[6]; if (fields(line, fl) < 6) continue; if (nrec == cap) { cap *= 2; recs = realloc(recs, cap * sizeof(Rec)); } size_t kl = strlen(fl[0]) + strlen(fl[3]) + strlen(fl[4]) + 3; char *key = malloc(kl); snprintf(key, kl, "%s\t%s\t%s", fl[0], fl[3], fl[4]); size_t cl = strlen(fl[3]) + strlen(fl[4]) + 2; char *cv = malloc(cl); snprintf(cv, cl, "%s:%s", fl[3], fl[4]); recs[nrec].key = key; recs[nrec].cv = cv; recs[nrec].text = fl[5]; nrec++; } qsort(recs, nrec, sizeof(Rec), cmp); /* resolve each key line to "chap:verse text\n" */ size_t obuf = 1 << 20, olen = 0; char *out = malloc(obuf); size_t missing = 0, nkeys = 0; for (char *line = strtok(keys, "\n"); line; line = strtok(NULL, "\n")) { nkeys++; Rec probe; probe.key = line; Rec *r = bsearch(&probe, recs, nrec, sizeof(Rec), cmp); const char *cv = "0:0", *text = ""; if (r) { cv = r->cv; text = r->text; } else { missing++; /* keep a placeholder so indices stay aligned */ } size_t need = strlen(cv) + strlen(text) + 3; while (olen + need > obuf) { obuf *= 2; out = realloc(out, obuf); } /* "chap:verse " then the text with runs of whitespace collapsed to one * space and the ends trimmed -- matching lectio's Fields-based render, so * the source's stray double spaces don't leak through. */ { size_t cl = strlen(cv); int sp = 0, started = 0; const char *p; memcpy(out + olen, cv, cl); olen += cl; out[olen++] = ' '; for (p = text; *p; p++) { if (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') { sp = 1; } else { if (sp && started) out[olen++] = ' '; sp = 0; started = 1; out[olen++] = *p; } } out[olen++] = '\n'; } } if (missing) fprintf(stderr, "mktext: warning: %zu of %zu verses not in corpus (blank)\n", missing, nkeys); /* Pack: gzip the raw text, then strip the gzip container to the bare DEFLATE * stream (10-byte header + 8-byte trailer) that clectio's puff() decodes. * out.lz = [uint32 LE raw length][raw DEFLATE]. */ { char rawpath[4096], gzpath[4200], cmd[4300]; FILE *rf, *of; char *gz; size_t gzn, deflen; unsigned char hdr[4]; snprintf(rawpath, sizeof rawpath, "%s.raw", argv[3]); rf = fopen(rawpath, "wb"); if (!rf) { fprintf(stderr, "mktext: cannot write %s\n", rawpath); return 1; } fwrite(out, 1, olen, rf); fclose(rf); snprintf(cmd, sizeof cmd, "gzip -9nf '%s'", rawpath); if (system(cmd) != 0) { fprintf(stderr, "mktext: gzip failed (is gzip installed?)\n"); return 1; } snprintf(gzpath, sizeof gzpath, "%s.gz", rawpath); gz = slurp(gzpath, &gzn); remove(gzpath); if (gzn < 18) { fprintf(stderr, "mktext: unexpected gzip output\n"); return 1; } deflen = gzn - 18; /* drop 10-byte gzip header + 8-byte trailer */ of = fopen(argv[3], "wb"); if (!of) { fprintf(stderr, "mktext: cannot write %s\n", argv[3]); return 1; } hdr[0] = (unsigned char)olen; hdr[1] = (unsigned char)(olen >> 8); hdr[2] = (unsigned char)(olen >> 16); hdr[3] = (unsigned char)(olen >> 24); fwrite(hdr, 1, 4, of); fwrite(gz + 10, 1, deflen, of); fclose(of); fprintf(stderr, "mktext: %zu verses, %zu -> %zu bytes (%.1fx)\n", nkeys, olen, deflen + 4, olen ? (double)olen / (double)(deflen + 4) : 0); } return 0; }