chiark / gitweb /
help menu can now pop up the man page
authorRichard Kettlewell <rjk@greenend.org.uk>
Sun, 21 Oct 2007 19:13:18 +0000 (20:13 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Sun, 21 Oct 2007 19:13:18 +0000 (20:13 +0100)
12 files changed:
.bzrignore
disobedience/Makefile.am
disobedience/disobedience.h
disobedience/help.c [new file with mode: 0644]
disobedience/menu.c
doc/Makefile.am
lib/Makefile.am
lib/charset.c
lib/charset.h
lib/html.c [new file with mode: 0644]
lib/html.h [new file with mode: 0644]
scripts/htmlman

index 103b31db265c6115ec6608eb9b090582ea8c93a7..2625610af7f1ab78c97534df4bf782f436418dad 100644 (file)
@@ -109,3 +109,5 @@ doc/disorder-normalize.8.html
 doc/disorder-decode.8.html
 doc/disorder-decode.8
 doc/plumbing.png
+disobedience/manual.h
+disobedience/manual.html
index 86d1102bff99f38b6794284b2bf4e0ba743eba1b..561f8bb7e6d2755db1da4c3eb0e636be2ced02d4 100644 (file)
@@ -25,7 +25,7 @@ AM_CFLAGS=$(GLIB_CFLAGS) $(GTK_CFLAGS)
 
 disobedience_SOURCES=disobedience.h disobedience.c client.c queue.c    \
                  choose.c misc.c style.h control.c properties.c menu.c \
-                 log.c progress.c login.c rtp.c \
+                 log.c progress.c login.c rtp.c help.c \
                  ../lib/memgc.c
 disobedience_LDADD=../lib/libdisorder.a $(LIBPCRE) $(LIBGC) $(LIBGCRYPT)
 disobedience_LDFLAGS=$(GTK_LIBS)
@@ -38,8 +38,18 @@ check: check-help
 disobedience.o: style.h
 
 style.h: ${srcdir}/disobedience.rc ${top_srcdir}/scripts/text2c
-       ${top_srcdir}/scripts/text2c style ${srcdir}/disobedience.rc > style.h.tmp
-       mv style.h.tmp style.h
+       ${top_srcdir}/scripts/text2c style ${srcdir}/disobedience.rc > $@.tmp
+       mv $@.tmp $@
+
+manual.html: ../doc/disobedience.1 $(top_srcdir)/scripts/htmlman
+       rm -f $@.new
+       $(top_srcdir)/scripts/htmlman $< >$@.new
+       chmod 444 $@.new
+       mv -f $@.new $@
+
+manual.h: manual.html ${top_srcdir}/scripts/text2c
+       ${top_srcdir}/scripts/text2c manual manual.html > $@.tmp
+       mv $@.tmp $@
 
 EXTRA_DIST=disobedience.rc
 
index 884ebfe2827620960c283d69ab3f1d7be29ca3da..293a773c4bbca94f9e388841efd4840ad1c507d8 100644 (file)
@@ -235,6 +235,10 @@ void choose_update(void);
 
 void login_box(void);
 
+/* Help */
+
+void popup_help(void);
+
 /* RTP */
 
 int rtp_running(void);
diff --git a/disobedience/help.c b/disobedience/help.c
new file mode 100644 (file)
index 0000000..09de980
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include "disobedience.h"
+#include "table.h"
+#include "html.h"
+#include "manual.h"
+
+VECTOR_TYPE(markstack, GtkTextMark *, xrealloc);
+
+/** @brief Known tag type */
+struct tag {
+  /** @brief HTML tag name */
+  const char *name;
+
+  /** @brief Called to set up the tag */
+  void (*init)(GtkTextTag *tag);
+  
+  /** @brief GTK+ tag object */
+  GtkTextTag *tag;
+};
+
+static void init_bold(GtkTextTag *tag) {
+  g_object_set(G_OBJECT(tag), "weight", PANGO_WEIGHT_BOLD, (char *)0);
+}
+
+static void init_italic(GtkTextTag *tag) {
+  g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, (char *)0);
+}
+
+/** @brief Table of known tags
+ *
+ * Keep in alphabetical order
+ */
+static  struct tag tags[] = {
+  { "b", init_bold, 0 },
+  { "i", init_italic, 0 }
+};
+
+/** @brief Number of known tags */
+#define NTAGS (sizeof tags / sizeof *tags)
+
+/** @brief State structure for insert_html() */
+struct state {
+  /** @brief The buffer to insert into */
+  GtkTextBuffer *buffer;
+
+  /** @brief True if we are inside <body> */
+  int body;
+
+  /** @brief Stack of marks corresponding to tags */
+  struct markstack marks[1];
+
+};
+
+/** @brief Called for an open tag */
+static void html_open(const char *tag,
+                     hash attribute((unused)) *attrs,
+                     void *u) {
+  struct state *const s = u;
+  GtkTextIter iter[1];
+
+  if(!strcmp(tag, "body"))
+    s->body = 1;
+  if(!s->body)
+    return;
+  /* push a mark for the start of the region */
+  gtk_text_buffer_get_iter_at_mark(s->buffer, iter,
+                                  gtk_text_buffer_get_insert(s->buffer));
+  markstack_append(s->marks,
+                  gtk_text_buffer_create_mark(s->buffer,
+                                              0/* mark_name */,
+                                              iter,
+                                              TRUE/*left_gravity*/));
+}
+
+/** @brief Called for a close tag */
+static void html_close(const char *tag,
+                      void *u) {
+  struct state *const s = u;
+  GtkTextIter start[1], end[1];
+  int n;
+
+  if(!strcmp(tag, "body"))
+    s->body = 0;
+  if(!s->body)
+    return;
+  /* see if this is a known tag */
+  if((n = TABLE_FIND(tags, struct tag, name, tag)) < 0)
+    return;
+  /* pop the mark at the start of the region */
+  assert(s->marks->nvec > 0);
+  gtk_text_buffer_get_iter_at_mark(s->buffer, start,
+                                  s->marks->vec[--s->marks->nvec]);
+  gtk_text_buffer_get_iter_at_mark(s->buffer, end,
+                                  gtk_text_buffer_get_insert(s->buffer));
+  /* apply a tag */
+  gtk_text_buffer_apply_tag(s->buffer, tags[n].tag, start, end);
+  /* don't need the start mark any more */
+  gtk_text_buffer_delete_mark(s->buffer, s->marks->vec[s->marks->nvec]);
+}
+
+/** @brief Called for text */
+static void html_text(const char *text,
+                     void *u) {
+  struct state *const s = u;
+
+  /* ignore header */
+  if(!s->body)
+    return;
+  gtk_text_buffer_insert_at_cursor(s->buffer, text, strlen(text));
+}
+
+/** @brief Callbacks for insert_html() */
+static const struct html_parser_callbacks insert_html_callbacks = {
+  html_open,
+  html_close,
+  html_text
+};
+
+/** @brief Insert @p html into @p buffer at cursor */
+static void insert_html(GtkTextBuffer *buffer,
+                       const char *html) {
+  struct state s[1];
+  size_t n;
+  GtkTextTagTable *tagtable;
+
+  memset(s, 0, sizeof *s);
+  s->buffer = buffer;
+  markstack_init(s->marks);
+  /* initialize tags */
+  if(!tags[0].tag)
+    for(n = 0; n < NTAGS; ++n)
+      tags[n].init(tags[n].tag = gtk_text_tag_new(0));
+  /* add tags to this buffer */
+  tagtable = gtk_text_buffer_get_tag_table(s->buffer);
+  for(n = 0; n < NTAGS; ++n)
+    gtk_text_tag_table_add(tagtable, tags[n].tag);
+  /* convert the input */
+  html_parse(&insert_html_callbacks, html, s);
+}
+
+static GtkTextBuffer *html_buffer(const char *html) {
+  GtkTextBuffer *buffer = gtk_text_buffer_new(NULL);
+
+  insert_html(buffer, html);
+  return buffer;
+}
+
+static GtkWidget *help_window;
+
+void popup_help(void) {
+  GtkWidget *view;
+  
+  if(help_window) {
+    gtk_window_present(GTK_WINDOW(help_window));
+    return;
+  }
+  help_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+  g_signal_connect(help_window, "destroy",
+                  G_CALLBACK(gtk_widget_destroyed), &help_window);
+  gtk_window_set_title(GTK_WINDOW(help_window), "Disobedience Manual");
+  view = gtk_text_view_new_with_buffer(html_buffer(manual));
+  gtk_container_add(GTK_CONTAINER(help_window),
+                   scroll_widget(view,
+                                 "help"));
+  gtk_widget_show_all(help_window);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 843f45148565ea7a20c61ebc07496d356ae3a543..c5ace02375931326c257e615393cc1c54ae44759 100644 (file)
@@ -111,6 +111,14 @@ static void about_popup(gpointer attribute((unused)) callback_data,
                            0);
 }
 
+static void manual_popup(gpointer attribute((unused)) callback_data,
+                       guint attribute((unused)) callback_action,
+                       GtkWidget attribute((unused)) *menu_item) {
+  D(("manual_popup"));
+
+  popup_help();
+}
+
 /** @brief Callde when version arrives, displays about... popup */
 static void about_popup_got_version(void attribute((unused)) *v,
                                     const char *value) {
@@ -238,6 +246,14 @@ GtkWidget *menubar(GtkWidget *w) {
       (char *)"<Branch>",               /* item_type */
       0                                 /* extra_data */
     },
+    {
+      (char *)"/Help/Manual page",      /* path */
+      0,                                /* accelerator */
+      manual_popup,                     /* callback */
+      0,                                /* callback_action */
+      0,                                /* item_type */
+      0                                 /* extra_data */
+    },
     {
       (char *)"/Help/About DisOrder",   /* path */
       0,                                /* accelerator */
index 420d4eec5d0e2e54ec18d3b1518285b9b22ab31f..915d55a1e521d9c6f6d12bd2254fdfb4d30b3d2e 100644 (file)
@@ -36,7 +36,7 @@ HTMLMAN=$(foreach man,$(man_MANS),$(man).html)
 
 $(HTMLMAN) : %.html : % $(top_srcdir)/scripts/htmlman
        rm -f $@.new
-       $(top_srcdir)/scripts/htmlman $< >$@.new
+       $(top_srcdir)/scripts/htmlman -stdhead $< >$@.new
        chmod 444 $@.new
        mv -f $@.new $@
 
index 0216f05d7d3515389241398e13d59e4299b75dc2..e9b41f89055ecf967c745eae5eae5c0b7e6a1359 100644 (file)
@@ -38,6 +38,7 @@ libdisorder_a_SOURCES=charset.c charset.h             \
        hash.c hash.h                                   \
        heap.h                                          \
        hex.c hex.h                                     \
+       html.c html.h                                   \
        ifreq.c ifreq.h                                 \
        inputline.c inputline.h                         \
        kvp.c kvp.h                                     \
index 2a38fbf6397e5cea8291445ad4f4abc1560ef74d..205084f26aff0356737ec034d290d304aceb540f 100644 (file)
@@ -91,6 +91,33 @@ uint32_t *utf82ucs4(const char *mb) {
   return d.vec;
 }
 
+/** @brief Convert one UCS-4 character to UTF-8
+ * @param c Character to convert
+ * @param d Dynamic string to append UTF-8 sequence to
+ * @return 0 on success, -1 on error
+ */
+int one_ucs42utf8(uint32_t c, struct dynstr *d) {
+  if(c < 0x80)
+    dynstr_append(d, c);
+  else if(c < 0x800) {
+    dynstr_append(d, 0xC0 | (c >> 6));
+    dynstr_append(d, 0x80 | (c & 0x3F));
+  } else if(c < 0x10000) {
+    dynstr_append(d, 0xE0 | (c >> 12));
+    dynstr_append(d, 0x80 | ((c >> 6) & 0x3F));
+    dynstr_append(d, 0x80 | (c & 0x3F));
+  } else if(c < 0x110000) {
+    dynstr_append(d, 0xF0 | (c >> 18));
+    dynstr_append(d, 0x80 | ((c >> 12) & 0x3F));
+    dynstr_append(d, 0x80 | ((c >> 6) & 0x3F));
+    dynstr_append(d, 0x80 | (c & 0x3F));
+  } else {
+    error(0, "invalid UCS-4 character %#"PRIx32, c);
+    return -1;
+  }
+  return 0;
+}
+
 /** @brief Convert UCS-4 to UTF-8
  * @param u Pointer to 0-terminated UCS-4 string
  * @return Pointer to 0-terminated UTF-8 string
@@ -103,24 +130,8 @@ char *ucs42utf8(const uint32_t *u) {
 
   dynstr_init(&d);
   while((c = *u++)) {
-    if(c < 0x80)
-      dynstr_append(&d, c);
-    else if(c < 0x800) {
-      dynstr_append(&d, 0xC0 | (c >> 6));
-      dynstr_append(&d, 0x80 | (c & 0x3F));
-    } else if(c < 0x10000) {
-      dynstr_append(&d, 0xE0 | (c >> 12));
-      dynstr_append(&d, 0x80 | ((c >> 6) & 0x3F));
-      dynstr_append(&d, 0x80 | (c & 0x3F));
-    } else if(c < 0x110000) {
-      dynstr_append(&d, 0xF0 | (c >> 18));
-      dynstr_append(&d, 0x80 | ((c >> 12) & 0x3F));
-      dynstr_append(&d, 0x80 | ((c >> 6) & 0x3F));
-      dynstr_append(&d, 0x80 | (c & 0x3F));
-    } else {
-      error(0, "invalid UCS-4 character");
+    if(one_ucs42utf8(c, &d))
       return 0;
-    }
   }
   dynstr_terminate(&d);
   return d.vec;
index f77c58378be2002cc262e0cd85bb8a605d84d479..b172b76f745af0c0f99ff644ad0b0942b45a5ef8 100644 (file)
 #ifndef CHARSET_H
 #define CHARSET_H
 
+struct dynstr;
+
 /* Character encoding conversion routines */
 
+int one_ucs42utf8(uint32_t c, struct dynstr *d);
+
 uint32_t *utf82ucs4(const char *mb);
 char *ucs42utf8(const uint32_t *u);
 char *mb2utf8(const char *mb);
diff --git a/lib/html.c b/lib/html.c
new file mode 100644 (file)
index 0000000..7ce16d9
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file lib/html.c
+ * @brief Noddy HTML parser
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <string.h>
+#include <ctype.h>
+#include <stddef.h>
+
+#include "hash.h"
+#include "html.h"
+#include "mem.h"
+#include "log.h"
+#include "vector.h"
+#include "charset.h"
+#include "table.h"
+
+/** @brief Entity table type */
+struct entity {
+  const char *name;
+  uint32_t value;
+};
+
+/** @brief Known entities
+ *
+ * We only support the entities that turn up in the HTML files we
+ * actually care about.
+ *
+ * Keep in alphabetical order.
+ */
+static const struct entity entities[] = {
+  { "amp", '&' },
+  { "gt", '>' },
+  { "lt", '<' }
+};
+
+/** @brief Skip whitespace */
+static const char *skipwhite(const char *input) {
+  while(*input && isspace((unsigned char)*input))
+    ++input;
+  return input;
+}
+
+/** @brief Parse an entity */
+static const char *parse_entity(const char *input,
+                               uint32_t *entityp) {
+  input = skipwhite(input);
+  if(*input == '#') {
+    input = skipwhite(input + 1);
+    if(*input == 'x')
+      *entityp = strtoul(skipwhite(input + 1), (char **)&input, 16);
+    else
+      *entityp = strtoul(input, (char **)&input, 10);
+  } else {
+    struct dynstr name[1];
+    int n;
+
+    dynstr_init(name);
+    while(isalnum((unsigned char)*input))
+      dynstr_append(name, tolower((unsigned char)*input++));
+    dynstr_terminate(name);
+    if((n = TABLE_FIND(entities, struct entity, name, name->vec)) < 0) {
+      error(0, "unknown entity '%s'", name->vec);
+      *entityp = '?';
+    } else
+      *entityp = entities[n].value;
+  }
+  input = skipwhite(input);
+  if(*input == ';')
+    ++input;
+  return input;
+}
+
+/** @brief Parse one character or entity and append it to a @ref dynstr */
+static const char *parse_one(const char *input, struct dynstr *d) {
+  if(*input == '&') {
+    uint32_t c;
+    input = parse_entity(input + 1, &c);
+    if(one_ucs42utf8(c, d))
+      dynstr_append(d, '?');   /* U+FFFD might be a better choice */
+  } else
+    dynstr_append(d, *input++);
+  return input;
+}
+
+/** @brief Too-stupid-to-live HTML parser
+ * @param callbacks Parser callbacks
+ * @param input HTML document
+ * @param u User data pointer
+ * @return 0 on success, -1 on error
+ */
+int html_parse(const struct html_parser_callbacks *callbacks,
+              const char *input,
+              void *u) {
+  struct dynstr text[1];
+
+  dynstr_init(text);
+  while(*input) {
+    if(*input == '<') {
+      struct dynstr tag[1];
+      hash *attrs;
+
+      /* flush collected text */
+      if(text->nvec) {
+       dynstr_terminate(text);
+       callbacks->text(text->vec, u);
+       text->nvec = 0;
+      }
+      dynstr_init(tag);
+      input = skipwhite(input + 1);
+      /* see if it's an open or close tag */
+      if(*input == '/') {
+       input = skipwhite(input + 1);
+       attrs = 0;
+      } else
+       attrs = hash_new(sizeof(char *));
+      /* gather tag */
+      while(isalnum((unsigned char)*input))
+       dynstr_append(tag, tolower((unsigned char)*input++));
+      dynstr_terminate(tag);
+      input = skipwhite(input);
+      if(attrs) {
+       /* gather attributes */
+       while(*input && *input != '>') {
+         struct dynstr name[1], value[1];
+
+         dynstr_init(name);
+         dynstr_init(value);
+         /* attribute name */
+         while(isalnum((unsigned char)*input))
+           dynstr_append(name, tolower((unsigned char)*input++));
+         dynstr_terminate(name);       
+         input = skipwhite(input);
+         if(*input == '=') {
+           /* attribute value */
+           input = skipwhite(input + 1);
+           if(*input == '"' || *input == '\'') {
+             /* quoted value */
+             const int q = *input++;
+             while(*input && *input != q)
+               input = parse_one(input, value);
+             if(*input == q)
+               ++input;
+           } else {
+             /* unquoted value */
+             while(*input && *input != '>' && !isspace((unsigned char)*input))
+               input = parse_one(input, value);
+           }
+           dynstr_terminate(value);
+         }
+         /* stash the value */
+         hash_add(attrs, name->vec, value->vec, HASH_INSERT_OR_REPLACE);
+         input = skipwhite(input);
+       }
+      }
+      if(*input != '>') {
+       error(0, "unterminated tag %s", tag->vec);
+       return -1;
+      }
+      ++input;
+      if(attrs)
+       callbacks->open(tag->vec, attrs, u);
+      else
+       callbacks->close(tag->vec, u);
+    } else
+      input = parse_one(input, text);
+  }
+  /* flush any trailing text */
+  if(text->nvec) {
+    dynstr_terminate(text);
+    callbacks->text(text->vec, u);
+    text->nvec = 0;
+  }
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/html.h b/lib/html.h
new file mode 100644 (file)
index 0000000..fd81d1d
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file lib/html.c
+ * @brief Noddy HTML parser
+ */
+
+#ifndef HTML_H
+#define HTML_H
+
+/** @brief HTML parser callbacks */
+struct html_parser_callbacks {
+  /** @brief Called for an open tag
+   * @param tag Name of tag, normalized to lower case
+   * @param attrs Hash containing attributes
+   * @param u User data pointer
+   */
+  void (*open)(const char *tag,
+              hash *attrs,
+              void *u);
+
+  /** @brief Called for a close tag
+   * @param tag Name of tag, normalized to lower case
+   * @param u User data pointer
+   */
+  void (*close)(const char *tag,
+               void *u);
+
+  /** @brief Called for text
+   * @param text Pointer to text
+   * @param u User data pointer
+   */
+  void (*text)(const char *text,
+              void *u);
+};
+
+int html_parse(const struct html_parser_callbacks *callbacks,
+              const char *input,
+              void *u);
+
+#endif /* HTML_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index c579151dcd9679dba61e6e4cabd5e34288d1f13e..82a33ca20c822bb3650fb29f0261665d16470b62 100755 (executable)
 
 set -e
 
+stdhead=false
+
+while test $# -gt 0; do
+  case "$1" in
+  -stdhead )
+    stdhead=true
+    ;;
+  -* )
+    echo >&2 "ERROR: unknown option $1"
+    exit 1
+    ;;
+  * )
+    break
+  esac
+  shift
+done
+
 title=$(basename $1)
 
-cat <<EOF
-<html>
- <head>
-@include{stdhead}@
-  <title>$title</title>
- </head>
- <body>
-@include{@label{menu}@}@
-EOF
+echo "<html>"
+echo " <head>"
+if $stdhead; then
+  echo "@include{stdhead}@"
+fi
+echo "  <title>$title</title>"
+echo " </head>"
+echo " <body>"
+if $stdhead; then
+  echo "@include{@label{menu}@}@"
+fi
 printf "   <pre class=manpage>"
 # this is kind of painful using only BREs
-nroff -man "$1" | sed 's/&/\&amp;/g;
+nroff -man "$1" | sed \
+                      '1d;$d;
+                       s/&/\&amp;/g;
                        s/</\&lt;/g;
                        s/>/\&gt;/g;
                        s/@/\&#64;/g;
@@ -43,9 +64,9 @@ nroff -man "$1" | sed 's/&/\&amp;/g;
                        s!_\b\(.\)!<i>\1</i>!g;
                        s!_\b\(&[#0-9a-z][0-9a-z]*;\)!<i>\1</i>!g;
                        s!</\([bi]\)><\1>!!g'
-cat <<EOF
-</pre>
-@include{@label{menu}@end}@
- </body>
-</html>
-EOF
+echo "</pre>"
+if $stdhead; then
+  echo "@include{@label{menu}@end}@"
+fi
+echo " </body>"
+echo "</html>"