Add a \quote{...} directive, working like <blockquote> in HTML.
[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 case para_QuotePush:
245 nesting++;
246 break;
247 case para_LcontPop:
248 case para_QuotePop:
249 assert(nesting > 0);
250 nesting--;
251 break;
252
253 /*
254 * Chapter and section titles: start a new Help topic.
255 */
256 case para_Chapter:
257 case para_Appendix:
258 case para_UnnumberedChapter:
259 case para_Heading:
260 case para_Subsect:
261 if (lastsect && lastsect->child) {
262 paragraph *q;
263 /*
264 * Do a navigation menu for the previous section we
265 * were in.
266 */
267 for (q = lastsect->child; q; q = q->sibling)
268 whlp_navmenu(&state, q);
269 }
270 {
271 rdstringc rs = {0, 0, NULL};
272 WHLP_TOPIC new_topic, parent_topic;
273 char *macro, *topicid;
274
275 new_topic = p->private_data;
276 whlp_browse_link(h, state.curr_topic, new_topic);
277 state.curr_topic = new_topic;
278
279 if (p->kwtext) {
280 whlp_rdaddwc(&rs, p->kwtext);
281 rdaddsc(&rs, ": "); /* FIXME: configurability */
282 }
283 whlp_rdaddwc(&rs, p->words);
284 if (p->parent == NULL)
285 parent_topic = contents_topic;
286 else
287 parent_topic = (WHLP_TOPIC)p->parent->private_data;
288 topicid = whlp_topic_id(parent_topic);
289 macro = smalloc(100+strlen(topicid));
290 sprintf(macro,
291 "CBB(\"btn_up\",\"JI(`',`%s')\");EB(\"btn_up\")",
292 topicid);
293 whlp_begin_topic(h, new_topic,
294 rs.text ? rs.text : "",
295 macro, NULL);
296 sfree(macro);
297
298 {
299 /*
300 * Output the .cnt entry.
301 *
302 * WinHelp has a bug involving having an internal
303 * node followed by a leaf at the same level: the
304 * leaf is output at the wrong level. We can mostly
305 * work around this by modifying the leaf level
306 * itself (see whlp_contents_write), but this
307 * doesn't work for top-level sections since we
308 * can't turn a level-1 leaf into a level-0 one. So
309 * for top-level leaf sections (Bibliography
310 * springs to mind), we output an internal node
311 * containing only the leaf for that section.
312 */
313 int i;
314 paragraph *q;
315
316 /* Count up the level. */
317 i = 1;
318 for (q = p; q->parent; q = q->parent) i++;
319
320 if (p->child || !p->parent) {
321 /*
322 * If p has children then it needs to be a
323 * folder; if it has no parent then it needs to
324 * be a folder to work around the bug.
325 */
326 whlp_contents_write(&state, i, rs.text, NULL);
327 i++;
328 }
329 whlp_contents_write(&state, i, rs.text, new_topic);
330 }
331
332 sfree(rs.text);
333
334 whlp_begin_para(h, WHLP_PARA_NONSCROLL);
335 if (p->kwtext) {
336 whlp_mkparagraph(&state, FONT_TITLE, p->kwtext, FALSE);
337 whlp_set_font(h, FONT_TITLE);
338 whlp_text(h, ": "); /* FIXME: configurability */
339 }
340 whlp_mkparagraph(&state, FONT_TITLE, p->words, FALSE);
341 whlp_end_para(h);
342
343 lastsect = p;
344 }
345 break;
346
347 case para_Rule:
348 whlp_para_attr(h, WHLP_PARA_SPACEBELOW, 12);
349 whlp_para_attr(h, WHLP_PARA_ALIGNMENT, WHLP_ALIGN_CENTRE);
350 whlp_begin_para(h, WHLP_PARA_SCROLL);
351 whlp_set_font(h, FONT_RULE);
352 #define TEN "\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0"
353 #define TWENTY TEN TEN
354 #define FORTY TWENTY TWENTY
355 #define EIGHTY FORTY FORTY
356 whlp_text(h, EIGHTY);
357 #undef TEN
358 #undef TWENTY
359 #undef FORTY
360 #undef EIGHTY
361 whlp_end_para(h);
362 break;
363
364 case para_Normal:
365 case para_DescribedThing:
366 case para_Description:
367 case para_BiblioCited:
368 case para_Bullet:
369 case para_NumberedList:
370 whlp_para_attr(h, WHLP_PARA_SPACEBELOW, 12);
371 if (p->type == para_Bullet || p->type == para_NumberedList) {
372 whlp_para_attr(h, WHLP_PARA_LEFTINDENT, 72*nesting + 72);
373 whlp_para_attr(h, WHLP_PARA_FIRSTLINEINDENT, -36);
374 whlp_set_tabstop(h, 72, WHLP_ALIGN_LEFT);
375 whlp_begin_para(h, WHLP_PARA_SCROLL);
376 whlp_set_font(h, FONT_NORMAL);
377 if (p->type == para_Bullet) {
378 whlp_text(h, "\x95");
379 } else {
380 whlp_mkparagraph(&state, FONT_NORMAL, p->kwtext, FALSE);
381 whlp_text(h, ".");
382 }
383 whlp_tab(h);
384 } else {
385 whlp_para_attr(h, WHLP_PARA_LEFTINDENT,
386 72*nesting + (p->type==para_Description ? 72 : 0));
387 whlp_begin_para(h, WHLP_PARA_SCROLL);
388 }
389
390 if (p->type == para_BiblioCited) {
391 whlp_mkparagraph(&state, FONT_NORMAL, p->kwtext, FALSE);
392 whlp_text(h, " ");
393 }
394
395 whlp_mkparagraph(&state, FONT_NORMAL, p->words, FALSE);
396 whlp_end_para(h);
397 break;
398
399 case para_Code:
400 /*
401 * In a code paragraph, each individual word is a line. For
402 * Help files, we will have to output this as a set of
403 * paragraphs, all but the last of which don't set
404 * SPACEBELOW.
405 */
406 {
407 word *w;
408 wchar_t *t, *e;
409 char *c;
410
411 for (w = p->words; w; w = w->next) if (w->type == word_WeakCode) {
412 t = w->text;
413 if (w->next && w->next->type == word_Emph) {
414 w = w->next;
415 e = w->text;
416 } else
417 e = NULL;
418
419 if (!w->next)
420 whlp_para_attr(h, WHLP_PARA_SPACEBELOW, 12);
421
422 whlp_para_attr(h, WHLP_PARA_LEFTINDENT, 72*nesting);
423 whlp_begin_para(h, WHLP_PARA_SCROLL);
424 while (e && *e && *t) {
425 int n;
426 int ec = *e;
427
428 for (n = 0; t[n] && e[n] && e[n] == ec; n++);
429 if (ec == 'i')
430 whlp_set_font(h, FONT_ITAL_CODE);
431 else if (ec == 'b')
432 whlp_set_font(h, FONT_BOLD_CODE);
433 else
434 whlp_set_font(h, FONT_CODE);
435 whlp_convert(t, n, &c, FALSE);
436 whlp_text(h, c);
437 sfree(c);
438 t += n;
439 e += n;
440 }
441 whlp_set_font(h, FONT_CODE);
442 whlp_convert(t, 0, &c, FALSE);
443 whlp_text(h, c);
444 sfree(c);
445 whlp_end_para(h);
446 }
447 }
448 break;
449 }
450
451 fclose(state.cntfp);
452 whlp_close(h, filename);
453
454 /*
455 * Loop over the index entries, cleaning up our final text
456 * forms.
457 */
458 for (i = 0; (ie = index234(idx->entries, i)) != NULL; i++) {
459 sfree(ie->backend_data);
460 }
461 }
462
463 static void whlp_contents_write(struct bk_whlp_state *state,
464 int level, char *text, WHLP_TOPIC topic) {
465 /*
466 * Horrifying bug in WinHelp. When dropping a section level or
467 * more without using a folder-type entry, WinHelp accidentally
468 * adds one to the section level. So we correct for that here.
469 */
470 if (state->cnt_last_level > level && topic)
471 state->cnt_workaround = -1;
472 else if (!topic)
473 state->cnt_workaround = 0;
474 state->cnt_last_level = level;
475
476 fprintf(state->cntfp, "%d ", level + state->cnt_workaround);
477 while (*text) {
478 if (*text == '=')
479 fputc('\\', state->cntfp);
480 fputc(*text, state->cntfp);
481 text++;
482 }
483 if (topic)
484 fprintf(state->cntfp, "=%s", whlp_topic_id(topic));
485 fputc('\n', state->cntfp);
486 }
487
488 static void whlp_navmenu(struct bk_whlp_state *state, paragraph *p) {
489 whlp_begin_para(state->h, WHLP_PARA_NONSCROLL);
490 whlp_start_hyperlink(state->h, (WHLP_TOPIC)p->private_data);
491 if (p->kwtext) {
492 whlp_mkparagraph(state, FONT_NORMAL, p->kwtext, TRUE);
493 whlp_set_font(state->h, FONT_NORMAL);
494 whlp_text(state->h, ": "); /* FIXME: configurability */
495 }
496 whlp_mkparagraph(state, FONT_NORMAL, p->words, TRUE);
497 whlp_end_hyperlink(state->h);
498 whlp_end_para(state->h);
499
500 }
501
502 static void whlp_mkparagraph(struct bk_whlp_state *state,
503 int font, word *text, int subsidiary) {
504 keyword *kwl;
505 int deffont = font;
506 int currfont = -1;
507 int newfont;
508 char *c;
509 paragraph *xref_target = NULL;
510
511 for (; text; text = text->next) switch (text->type) {
512 case word_HyperLink:
513 case word_HyperEnd:
514 break;
515
516 case word_IndexRef:
517 if (subsidiary) break; /* disabled in subsidiary bits */
518 {
519 indextag *tag = index_findtag(state->idx, text->text);
520 int i;
521 if (!tag)
522 break;
523 for (i = 0; i < tag->nrefs; i++)
524 whlp_index_term(state->h, tag->refs[i]->backend_data,
525 state->curr_topic);
526 }
527 break;
528
529 case word_UpperXref:
530 case word_LowerXref:
531 if (subsidiary) break; /* disabled in subsidiary bits */
532 kwl = kw_lookup(state->keywords, text->text);
533 assert(xref_target == NULL);
534 if (kwl) {
535 if (kwl->para->type == para_NumberedList) {
536 break; /* don't xref to numbered list items */
537 } else if (kwl->para->type == para_BiblioCited) {
538 /*
539 * An xref to a bibliography item jumps to the section
540 * containing it.
541 */
542 if (kwl->para->parent)
543 xref_target = kwl->para->parent;
544 else
545 break;
546 } else {
547 xref_target = kwl->para;
548 }
549 whlp_start_hyperlink(state->h,
550 (WHLP_TOPIC)xref_target->private_data);
551 }
552 break;
553
554 case word_XrefEnd:
555 if (subsidiary) break; /* disabled in subsidiary bits */
556 if (xref_target)
557 whlp_end_hyperlink(state->h);
558 xref_target = NULL;
559 break;
560
561 case word_Normal:
562 case word_Emph:
563 case word_Code:
564 case word_WeakCode:
565 case word_WhiteSpace:
566 case word_EmphSpace:
567 case word_CodeSpace:
568 case word_WkCodeSpace:
569 case word_Quote:
570 case word_EmphQuote:
571 case word_CodeQuote:
572 case word_WkCodeQuote:
573 if (towordstyle(text->type) == word_Emph)
574 newfont = deffont + FONT_EMPH;
575 else if (towordstyle(text->type) == word_Code ||
576 towordstyle(text->type) == word_WeakCode)
577 newfont = deffont + FONT_CODE;
578 else
579 newfont = deffont;
580 if (newfont != currfont) {
581 currfont = newfont;
582 whlp_set_font(state->h, newfont);
583 }
584 if (removeattr(text->type) == word_Normal) {
585 if (whlp_convert(text->text, 0, &c, TRUE))
586 whlp_text(state->h, c);
587 else
588 whlp_mkparagraph(state, deffont, text->alt, FALSE);
589 sfree(c);
590 } else if (removeattr(text->type) == word_WhiteSpace) {
591 whlp_text(state->h, " ");
592 } else if (removeattr(text->type) == word_Quote) {
593 whlp_text(state->h,
594 quoteaux(text->aux) == quote_Open ? "\x91" : "\x92");
595 /* FIXME: configurability */
596 }
597 break;
598 }
599 }
600
601 static void whlp_rdaddwc(rdstringc *rs, word *text) {
602 char *c;
603
604 for (; text; text = text->next) switch (text->type) {
605 case word_HyperLink:
606 case word_HyperEnd:
607 case word_UpperXref:
608 case word_LowerXref:
609 case word_XrefEnd:
610 case word_IndexRef:
611 break;
612
613 case word_Normal:
614 case word_Emph:
615 case word_Code:
616 case word_WeakCode:
617 case word_WhiteSpace:
618 case word_EmphSpace:
619 case word_CodeSpace:
620 case word_WkCodeSpace:
621 case word_Quote:
622 case word_EmphQuote:
623 case word_CodeQuote:
624 case word_WkCodeQuote:
625 assert(text->type != word_CodeQuote &&
626 text->type != word_WkCodeQuote);
627 if (removeattr(text->type) == word_Normal) {
628 if (whlp_convert(text->text, 0, &c, FALSE))
629 rdaddsc(rs, c);
630 else
631 whlp_rdaddwc(rs, text->alt);
632 sfree(c);
633 } else if (removeattr(text->type) == word_WhiteSpace) {
634 rdaddc(rs, ' ');
635 } else if (removeattr(text->type) == word_Quote) {
636 rdaddc(rs, quoteaux(text->aux) == quote_Open ? '\x91' : '\x92');
637 /* FIXME: configurability */
638 }
639 break;
640 }
641 }
642
643 /*
644 * Convert a wide string into a string of chars. If `result' is
645 * non-NULL, mallocs the resulting string and stores a pointer to
646 * it in `*result'. If `result' is NULL, merely checks whether all
647 * characters in the string are feasible for the output character
648 * set.
649 *
650 * Return is nonzero if all characters are OK. If not all
651 * characters are OK but `result' is non-NULL, a result _will_
652 * still be generated!
653 */
654 static int whlp_convert(wchar_t *s, int maxlen,
655 char **result, int hard_spaces) {
656 /*
657 * FIXME. Currently this is ISO8859-1 only.
658 */
659 int doing = (result != 0);
660 int ok = TRUE;
661 char *p = NULL;
662 int plen = 0, psize = 0;
663
664 if (maxlen <= 0)
665 maxlen = -1;
666
667 for (; *s && maxlen != 0; s++, maxlen--) {
668 wchar_t c = *s;
669 char outc;
670
671 if ((c >= 32 && c <= 126) ||
672 (c >= 160 && c <= 255)) {
673 /* Char is OK. */
674 if (c == 32 && hard_spaces)
675 outc = '\240';
676 else
677 outc = (char)c;
678 } else {
679 /* Char is not OK. */
680 ok = FALSE;
681 outc = 0xBF; /* approximate the good old DEC `uh?' */
682 }
683 if (doing) {
684 if (plen >= psize) {
685 psize = plen + 256;
686 p = resize(p, psize);
687 }
688 p[plen++] = outc;
689 }
690 }
691 if (doing) {
692 p = resize(p, plen+1);
693 p[plen] = '\0';
694 *result = p;
695 }
696 return ok;
697 }