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