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