Rename Buttress to Halibut. I _think_ I've caught everything in this pass.
[sgt/halibut] / bk_whlp.c
1 /*
2 * Windows Help backend for Halibut
3 *
4 * TODO:
5 * - allow user to specify section contexts.
6 */
7
8 #include <stdio.h>
9 #include <stdlib.h>
10 #include <assert.h>
11
12 #include "halibut.h"
13 #include "winhelp.h"
14
15 struct bk_whlp_state {
16 WHLP h;
17 indexdata *idx;
18 keywordlist *keywords;
19 WHLP_TOPIC curr_topic;
20 FILE *cntfp;
21 int cnt_last_level, cnt_workaround;
22 };
23
24 /*
25 * Indexes of fonts in our standard font descriptor set.
26 */
27 enum {
28 FONT_NORMAL,
29 FONT_EMPH,
30 FONT_CODE,
31 FONT_TITLE,
32 FONT_TITLE_EMPH,
33 FONT_TITLE_CODE,
34 FONT_RULE
35 };
36
37 static void whlp_rdaddwc(rdstringc *rs, word *text);
38 static int whlp_convert(wchar_t *s, char **result, int hard_spaces);
39 static void whlp_mkparagraph(struct bk_whlp_state *state,
40 int font, word *text, int subsidiary);
41 static void whlp_navmenu(struct bk_whlp_state *state, paragraph *p);
42 static void whlp_contents_write(struct bk_whlp_state *state,
43 int level, char *text, WHLP_TOPIC topic);
44
45 void whlp_backend(paragraph *sourceform, keywordlist *keywords,
46 indexdata *idx) {
47 WHLP h;
48 char *filename, *cntname;
49 paragraph *p, *lastsect;
50 struct bk_whlp_state state;
51 WHLP_TOPIC contents_topic;
52 int i;
53 indexentry *ie;
54
55 filename = "output.hlp"; /* FIXME: configurability */
56 cntname = "output.cnt"; /* corresponding contents file */
57
58 state.cntfp = fopen(cntname, "wb");
59 state.cnt_last_level = -1; state.cnt_workaround = 0;
60
61 h = state.h = whlp_new();
62 state.keywords = keywords;
63 state.idx = idx;
64
65 whlp_start_macro(h, "CB(\"btn_about\",\"&About\",\"About()\")");
66 whlp_start_macro(h, "CB(\"btn_up\",\"&Up\",\"Contents()\")");
67 whlp_start_macro(h, "BrowseButtons()");
68
69 whlp_create_font(h, "Times New Roman", WHLP_FONTFAM_SERIF, 24,
70 0, 0, 0, 0);
71 whlp_create_font(h, "Times New Roman", WHLP_FONTFAM_SERIF, 24,
72 WHLP_FONT_ITALIC, 0, 0, 0);
73 whlp_create_font(h, "Courier New", WHLP_FONTFAM_FIXED, 24,
74 0, 0, 0, 0);
75 whlp_create_font(h, "Arial", WHLP_FONTFAM_SERIF, 30,
76 WHLP_FONT_BOLD, 0, 0, 0);
77 whlp_create_font(h, "Arial", WHLP_FONTFAM_SERIF, 30,
78 WHLP_FONT_BOLD|WHLP_FONT_ITALIC, 0, 0, 0);
79 whlp_create_font(h, "Courier New", WHLP_FONTFAM_FIXED, 30,
80 WHLP_FONT_BOLD, 0, 0, 0);
81 whlp_create_font(h, "Courier New", WHLP_FONTFAM_SANS, 18,
82 WHLP_FONT_STRIKEOUT, 0, 0, 0);
83
84 /*
85 * Loop over the source form finding out whether the user has
86 * specified particular help topic names for anything.
87 */
88 for (p = sourceform; p; p = p->next) {
89 p->private_data = NULL;
90 if (p->type == para_Config && p->parent) {
91 if (!ustricmp(p->keyword, L"winhelp-topic")) {
92 char *topicname;
93 whlp_convert(uadv(p->keyword), &topicname, 0);
94 /* Store the topic name in the private_data field of the
95 * containing section. */
96 p->parent->private_data = topicname;
97 }
98 }
99 }
100
101 /*
102 * Loop over the source form registering WHLP_TOPICs for
103 * everything.
104 */
105
106 contents_topic = whlp_register_topic(h, "Top", NULL);
107 whlp_primary_topic(h, contents_topic);
108 for (p = sourceform; p; p = p->next) {
109 if (p->type == para_Chapter ||
110 p->type == para_Appendix ||
111 p->type == para_UnnumberedChapter ||
112 p->type == para_Heading ||
113 p->type == para_Subsect) {
114 char *topicid = p->private_data;
115 char *errstr;
116
117 p->private_data = whlp_register_topic(h, topicid, &errstr);
118 if (!p->private_data) {
119 p->private_data = whlp_register_topic(h, NULL, NULL);
120 error(err_winhelp_ctxclash, &p->fpos, topicid, errstr);
121 }
122 sfree(topicid);
123 }
124 }
125
126 /*
127 * Loop over the index entries, preparing final text forms for
128 * each one.
129 */
130 for (i = 0; (ie = index234(idx->entries, i)) != NULL; i++) {
131 rdstringc rs = {0, 0, NULL};
132 whlp_rdaddwc(&rs, ie->text);
133 ie->backend_data = rs.text;
134 }
135
136 whlp_prepare(h);
137
138 /* ------------------------------------------------------------------
139 * Do the contents page, containing title, preamble and
140 * copyright.
141 */
142
143 whlp_begin_topic(h, contents_topic, "Contents", "DB(\"btn_up\")", NULL);
144
145 /*
146 * The manual title goes in the non-scroll region, and also
147 * goes into the system title slot.
148 */
149 {
150 rdstringc rs = {0, 0, NULL};
151 for (p = sourceform; p; p = p->next) {
152 if (p->type == para_Title) {
153 whlp_begin_para(h, WHLP_PARA_NONSCROLL);
154 whlp_mkparagraph(&state, FONT_TITLE, p->words, FALSE);
155 whlp_rdaddwc(&rs, p->words);
156 whlp_end_para(h);
157 }
158 }
159 if (rs.text) {
160 whlp_title(h, rs.text);
161 fprintf(state.cntfp, ":Title %s\r\n", rs.text);
162 sfree(rs.text);
163 }
164 whlp_contents_write(&state, 1, "Title page", contents_topic);
165 /* FIXME: configurability in that string */
166 }
167
168 /*
169 * Next comes the preamble, which just goes into the ordinary
170 * scrolling region.
171 */
172 for (p = sourceform; p; p = p->next) {
173 if (p->type == para_Preamble) {
174 whlp_para_attr(h, WHLP_PARA_SPACEBELOW, 12);
175 whlp_begin_para(h, WHLP_PARA_SCROLL);
176 whlp_mkparagraph(&state, FONT_NORMAL, p->words, FALSE);
177 whlp_end_para(h);
178 }
179 }
180
181 /*
182 * The copyright goes to two places, again: into the contents
183 * page and also into the system section.
184 */
185 {
186 rdstringc rs = {0, 0, NULL};
187 for (p = sourceform; p; p = p->next) {
188 if (p->type == para_Copyright) {
189 whlp_para_attr(h, WHLP_PARA_SPACEBELOW, 12);
190 whlp_begin_para(h, WHLP_PARA_SCROLL);
191 whlp_mkparagraph(&state, FONT_NORMAL, p->words, FALSE);
192 whlp_end_para(h);
193 whlp_rdaddwc(&rs, p->words);
194 }
195 }
196 if (rs.text) {
197 whlp_copyright(h, rs.text);
198 sfree(rs.text);
199 }
200 }
201
202 /*
203 * Now do the primary navigation menu.
204 */
205 for (p = sourceform; p; p = p->next) {
206 if (p->type == para_Chapter ||
207 p->type == para_Appendix ||
208 p->type == para_UnnumberedChapter)
209 whlp_navmenu(&state, p);
210 }
211
212 state.curr_topic = contents_topic;
213 lastsect = NULL;
214
215 /* ------------------------------------------------------------------
216 * Now we've done the contents page, we're ready to go through
217 * and do the main manual text. Ooh.
218 */
219 for (p = sourceform; p; p = p->next) switch (p->type) {
220 /*
221 * Things we ignore because we've already processed them or
222 * aren't going to touch them in this pass.
223 */
224 case para_IM:
225 case para_BR:
226 case para_Biblio: /* only touch BiblioCited */
227 case para_VersionID:
228 case para_Copyright:
229 case para_Preamble:
230 case para_NoCite:
231 case para_Title:
232 break;
233
234 /*
235 * Chapter and section titles: start a new Help topic.
236 */
237 case para_Chapter:
238 case para_Appendix:
239 case para_UnnumberedChapter:
240 case para_Heading:
241 case para_Subsect:
242 if (lastsect && lastsect->child) {
243 paragraph *q;
244 /*
245 * Do a navigation menu for the previous section we
246 * were in.
247 */
248 for (q = lastsect->child; q; q = q->sibling)
249 whlp_navmenu(&state, q);
250 }
251 {
252 rdstringc rs = {0, 0, NULL};
253 WHLP_TOPIC new_topic, parent_topic;
254 char *macro, *topicid;
255
256 new_topic = p->private_data;
257 whlp_browse_link(h, state.curr_topic, new_topic);
258 state.curr_topic = new_topic;
259
260 if (p->kwtext) {
261 whlp_rdaddwc(&rs, p->kwtext);
262 rdaddsc(&rs, ": "); /* FIXME: configurability */
263 }
264 whlp_rdaddwc(&rs, p->words);
265 if (p->parent == NULL)
266 parent_topic = contents_topic;
267 else
268 parent_topic = (WHLP_TOPIC)p->parent->private_data;
269 topicid = whlp_topic_id(parent_topic);
270 macro = smalloc(100+strlen(topicid));
271 sprintf(macro,
272 "CBB(\"btn_up\",\"JI(`',`%s')\");EB(\"btn_up\")",
273 topicid);
274 whlp_begin_topic(h, new_topic,
275 rs.text ? rs.text : "",
276 macro, NULL);
277 sfree(macro);
278
279 {
280 /*
281 * Output the .cnt entry.
282 *
283 * WinHelp has a bug involving having an internal
284 * node followed by a leaf at the same level: the
285 * leaf is output at the wrong level. We can mostly
286 * work around this by modifying the leaf level
287 * itself (see whlp_contents_write), but this
288 * doesn't work for top-level sections since we
289 * can't turn a level-1 leaf into a level-0 one. So
290 * for top-level leaf sections (Bibliography
291 * springs to mind), we output an internal node
292 * containing only the leaf for that section.
293 */
294 int i;
295 paragraph *q;
296
297 /* Count up the level. */
298 i = 1;
299 for (q = p; q->parent; q = q->parent) i++;
300
301 if (p->child || !p->parent) {
302 /*
303 * If p has children then it needs to be a
304 * folder; if it has no parent then it needs to
305 * be a folder to work around the bug.
306 */
307 whlp_contents_write(&state, i, rs.text, NULL);
308 i++;
309 }
310 whlp_contents_write(&state, i, rs.text, new_topic);
311 }
312
313 sfree(rs.text);
314
315 whlp_begin_para(h, WHLP_PARA_NONSCROLL);
316 if (p->kwtext) {
317 whlp_mkparagraph(&state, FONT_TITLE, p->kwtext, FALSE);
318 whlp_set_font(h, FONT_TITLE);
319 whlp_text(h, ": "); /* FIXME: configurability */
320 }
321 whlp_mkparagraph(&state, FONT_TITLE, p->words, FALSE);
322 whlp_end_para(h);
323
324 lastsect = p;
325 }
326 break;
327
328 case para_Rule:
329 whlp_para_attr(h, WHLP_PARA_SPACEBELOW, 12);
330 whlp_para_attr(h, WHLP_PARA_ALIGNMENT, WHLP_ALIGN_CENTRE);
331 whlp_begin_para(h, WHLP_PARA_SCROLL);
332 whlp_set_font(h, FONT_RULE);
333 #define TEN "\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0"
334 #define TWENTY TEN TEN
335 #define FORTY TWENTY TWENTY
336 #define EIGHTY FORTY FORTY
337 whlp_text(h, EIGHTY);
338 #undef TEN
339 #undef TWENTY
340 #undef FORTY
341 #undef EIGHTY
342 whlp_end_para(h);
343 break;
344
345 case para_Normal:
346 case para_BiblioCited:
347 case para_Bullet:
348 case para_NumberedList:
349 whlp_para_attr(h, WHLP_PARA_SPACEBELOW, 12);
350 if (p->type == para_Bullet || p->type == para_NumberedList) {
351 whlp_para_attr(h, WHLP_PARA_LEFTINDENT, 72);
352 whlp_para_attr(h, WHLP_PARA_FIRSTLINEINDENT, -36);
353 whlp_set_tabstop(h, 72, WHLP_ALIGN_LEFT);
354 whlp_begin_para(h, WHLP_PARA_SCROLL);
355 whlp_set_font(h, FONT_NORMAL);
356 if (p->type == para_Bullet) {
357 whlp_text(h, "\x95");
358 } else {
359 whlp_mkparagraph(&state, FONT_NORMAL, p->kwtext, FALSE);
360 whlp_text(h, ".");
361 }
362 whlp_tab(h);
363 } else {
364 whlp_begin_para(h, WHLP_PARA_SCROLL);
365 }
366
367 if (p->type == para_BiblioCited) {
368 whlp_mkparagraph(&state, FONT_NORMAL, p->kwtext, FALSE);
369 whlp_text(h, " ");
370 }
371
372 whlp_mkparagraph(&state, FONT_NORMAL, p->words, FALSE);
373 whlp_end_para(h);
374 break;
375
376 case para_Code:
377 /*
378 * In a code paragraph, each individual word is a line. For
379 * Help files, we will have to output this as a set of
380 * paragraphs, all but the last of which don't set
381 * SPACEBELOW.
382 */
383 {
384 word *w;
385 char *c;
386 for (w = p->words; w; w = w->next) {
387 if (!w->next)
388 whlp_para_attr(h, WHLP_PARA_SPACEBELOW, 12);
389 whlp_begin_para(h, WHLP_PARA_SCROLL);
390 whlp_set_font(h, FONT_CODE);
391 whlp_convert(w->text, &c, FALSE);
392 whlp_text(h, c);
393 sfree(c);
394 whlp_end_para(h);
395 }
396 }
397 break;
398 }
399
400 fclose(state.cntfp);
401 whlp_close(h, filename);
402
403 /*
404 * Loop over the index entries, cleaning up our final text
405 * forms.
406 */
407 for (i = 0; (ie = index234(idx->entries, i)) != NULL; i++) {
408 sfree(ie->backend_data);
409 }
410 }
411
412 static void whlp_contents_write(struct bk_whlp_state *state,
413 int level, char *text, WHLP_TOPIC topic) {
414 /*
415 * Horrifying bug in WinHelp. When dropping a section level or
416 * more without using a folder-type entry, WinHelp accidentally
417 * adds one to the section level. So we correct for that here.
418 */
419 if (state->cnt_last_level > level && topic)
420 state->cnt_workaround = -1;
421 else if (!topic)
422 state->cnt_workaround = 0;
423 state->cnt_last_level = level;
424
425 fprintf(state->cntfp, "%d ", level + state->cnt_workaround);
426 while (*text) {
427 if (*text == '=')
428 fputc('\\', state->cntfp);
429 fputc(*text, state->cntfp);
430 text++;
431 }
432 if (topic)
433 fprintf(state->cntfp, "=%s", whlp_topic_id(topic));
434 fputc('\n', state->cntfp);
435 }
436
437 static void whlp_navmenu(struct bk_whlp_state *state, paragraph *p) {
438 whlp_begin_para(state->h, WHLP_PARA_NONSCROLL);
439 whlp_start_hyperlink(state->h, (WHLP_TOPIC)p->private_data);
440 if (p->kwtext) {
441 whlp_mkparagraph(state, FONT_NORMAL, p->kwtext, TRUE);
442 whlp_set_font(state->h, FONT_NORMAL);
443 whlp_text(state->h, ": "); /* FIXME: configurability */
444 }
445 whlp_mkparagraph(state, FONT_NORMAL, p->words, TRUE);
446 whlp_end_hyperlink(state->h);
447 whlp_end_para(state->h);
448
449 }
450
451 static void whlp_mkparagraph(struct bk_whlp_state *state,
452 int font, word *text, int subsidiary) {
453 keyword *kwl;
454 int deffont = font;
455 int currfont = -1;
456 int newfont;
457 char *c;
458 paragraph *xref_target = NULL;
459
460 for (; text; text = text->next) switch (text->type) {
461 case word_HyperLink:
462 case word_HyperEnd:
463 break;
464
465 case word_IndexRef:
466 if (subsidiary) break; /* disabled in subsidiary bits */
467 {
468 indextag *tag = index_findtag(state->idx, text->text);
469 int i;
470 if (!tag)
471 break;
472 for (i = 0; i < tag->nrefs; i++)
473 whlp_index_term(state->h, tag->refs[i]->backend_data,
474 state->curr_topic);
475 }
476 break;
477
478 case word_UpperXref:
479 case word_LowerXref:
480 if (subsidiary) break; /* disabled in subsidiary bits */
481 kwl = kw_lookup(state->keywords, text->text);
482 assert(xref_target == NULL);
483 if (kwl->para->type == para_NumberedList) {
484 break; /* don't xref to numbered list items */
485 } else if (kwl->para->type == para_BiblioCited) {
486 /*
487 * An xref to a bibliography item jumps to the section
488 * containing it.
489 */
490 if (kwl->para->parent)
491 xref_target = kwl->para->parent;
492 else
493 break;
494 } else {
495 xref_target = kwl->para;
496 }
497 whlp_start_hyperlink(state->h, (WHLP_TOPIC)xref_target->private_data);
498 break;
499
500 case word_XrefEnd:
501 if (subsidiary) break; /* disabled in subsidiary bits */
502 if (xref_target)
503 whlp_end_hyperlink(state->h);
504 xref_target = NULL;
505 break;
506
507 case word_Normal:
508 case word_Emph:
509 case word_Code:
510 case word_WeakCode:
511 case word_WhiteSpace:
512 case word_EmphSpace:
513 case word_CodeSpace:
514 case word_WkCodeSpace:
515 case word_Quote:
516 case word_EmphQuote:
517 case word_CodeQuote:
518 case word_WkCodeQuote:
519 if (towordstyle(text->type) == word_Emph)
520 newfont = deffont + FONT_EMPH;
521 else if (towordstyle(text->type) == word_Code ||
522 towordstyle(text->type) == word_WeakCode)
523 newfont = deffont + FONT_CODE;
524 else
525 newfont = deffont;
526 if (newfont != currfont) {
527 currfont = newfont;
528 whlp_set_font(state->h, newfont);
529 }
530 if (removeattr(text->type) == word_Normal) {
531 if (whlp_convert(text->text, &c, TRUE))
532 whlp_text(state->h, c);
533 else
534 whlp_mkparagraph(state, deffont, text->alt, FALSE);
535 sfree(c);
536 } else if (removeattr(text->type) == word_WhiteSpace) {
537 whlp_text(state->h, " ");
538 } else if (removeattr(text->type) == word_Quote) {
539 whlp_text(state->h,
540 quoteaux(text->aux) == quote_Open ? "\x91" : "\x92");
541 /* FIXME: configurability */
542 }
543 break;
544 }
545 }
546
547 static void whlp_rdaddwc(rdstringc *rs, word *text) {
548 char *c;
549
550 for (; text; text = text->next) switch (text->type) {
551 case word_HyperLink:
552 case word_HyperEnd:
553 case word_UpperXref:
554 case word_LowerXref:
555 case word_XrefEnd:
556 case word_IndexRef:
557 break;
558
559 case word_Normal:
560 case word_Emph:
561 case word_Code:
562 case word_WeakCode:
563 case word_WhiteSpace:
564 case word_EmphSpace:
565 case word_CodeSpace:
566 case word_WkCodeSpace:
567 case word_Quote:
568 case word_EmphQuote:
569 case word_CodeQuote:
570 case word_WkCodeQuote:
571 assert(text->type != word_CodeQuote &&
572 text->type != word_WkCodeQuote);
573 if (removeattr(text->type) == word_Normal) {
574 if (whlp_convert(text->text, &c, FALSE))
575 rdaddsc(rs, c);
576 else
577 whlp_rdaddwc(rs, text->alt);
578 sfree(c);
579 } else if (removeattr(text->type) == word_WhiteSpace) {
580 rdaddc(rs, ' ');
581 } else if (removeattr(text->type) == word_Quote) {
582 rdaddc(rs, quoteaux(text->aux) == quote_Open ? '\x91' : '\x92');
583 /* FIXME: configurability */
584 }
585 break;
586 }
587 }
588
589 /*
590 * Convert a wide string into a string of chars. If `result' is
591 * non-NULL, mallocs the resulting string and stores a pointer to
592 * it in `*result'. If `result' is NULL, merely checks whether all
593 * characters in the string are feasible for the output character
594 * set.
595 *
596 * Return is nonzero if all characters are OK. If not all
597 * characters are OK but `result' is non-NULL, a result _will_
598 * still be generated!
599 */
600 static int whlp_convert(wchar_t *s, char **result, int hard_spaces) {
601 /*
602 * FIXME. Currently this is ISO8859-1 only.
603 */
604 int doing = (result != 0);
605 int ok = TRUE;
606 char *p = NULL;
607 int plen = 0, psize = 0;
608
609 for (; *s; s++) {
610 wchar_t c = *s;
611 char outc;
612
613 if ((c >= 32 && c <= 126) ||
614 (c >= 160 && c <= 255)) {
615 /* Char is OK. */
616 if (c == 32 && hard_spaces)
617 outc = '\240';
618 else
619 outc = (char)c;
620 } else {
621 /* Char is not OK. */
622 ok = FALSE;
623 outc = 0xBF; /* approximate the good old DEC `uh?' */
624 }
625 if (doing) {
626 if (plen >= psize) {
627 psize = plen + 256;
628 p = resize(p, psize);
629 }
630 p[plen++] = outc;
631 }
632 }
633 if (doing) {
634 p = resize(p, plen+1);
635 p[plen] = '\0';
636 *result = p;
637 }
638 return ok;
639 }