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