summaryrefslogtreecommitdiff
path: root/mktext.c
blob: 17d7705f10ef53db9e1c583505904517c267ccb3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>

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;
}