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