Home | History | Annotate | Line # | Download | only in makeinfo
      1 /*	$NetBSD: html.c,v 1.2 2020/06/05 12:47:28 rin Exp $	*/
      2 
      3 /* html.c -- html-related utilities.
      4    Id: html.c,v 1.28 2004/12/06 01:13:06 karl Exp
      5 
      6    Copyright (C) 1999, 2000, 2001, 2002, 2003, 2004 Free Software
      7    Foundation, Inc.
      8 
      9    This program is free software; you can redistribute it and/or modify
     10    it under the terms of the GNU General Public License as published by
     11    the Free Software Foundation; either version 2, or (at your option)
     12    any later version.
     13 
     14    This program is distributed in the hope that it will be useful,
     15    but WITHOUT ANY WARRANTY; without even the implied warranty of
     16    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     17    GNU General Public License for more details.
     18 
     19    You should have received a copy of the GNU General Public License
     20    along with this program; if not, write to the Free Software Foundation,
     21    Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */
     22 
     23 #include "system.h"
     24 #include "cmds.h"
     25 #include "files.h"
     26 #include "html.h"
     27 #include "lang.h"
     28 #include "makeinfo.h"
     29 #include "node.h"
     30 #include "sectioning.h"
     31 
     32 
     33 /* Append CHAR to BUFFER, (re)allocating as necessary.  We don't handle
     35    null characters.  */
     36 
     37 typedef struct
     38 {
     39   unsigned size;    /* allocated */
     40   unsigned length;  /* used */
     41   char *buffer;
     42 } buffer_type;
     43 
     44 static buffer_type *
     45 init_buffer (void)
     46 {
     47   buffer_type *buf = xmalloc (sizeof (buffer_type));
     48   buf->length = 0;
     49   buf->size = 0;
     50   buf->buffer = NULL;
     51 
     52   return buf;
     53 }
     54 
     55 static void
     56 append_char (buffer_type *buf, int c)
     57 {
     58   buf->length++;
     59   if (buf->length >= buf->size)
     60     {
     61       buf->size += 100;
     62       buf->buffer = xrealloc (buf->buffer, buf->size);
     63     }
     64   buf->buffer[buf->length - 1] = c;
     65   buf->buffer[buf->length] = 0;
     66 }
     67 
     68 /* Read the cascading style-sheet file FILENAME.  Write out any @import
     69    commands, which must come first, by the definition of css.  If the
     70    file contains any actual css code following the @imports, return it;
     71    else return NULL.  */
     72 static char *
     73 process_css_file (char *filename)
     74 {
     75   int c;
     76   int lastchar = 0;
     77   FILE *f;
     78   buffer_type *import_text = init_buffer ();
     79   buffer_type *inline_text = init_buffer ();
     80   unsigned lineno = 1;
     81   enum { null_state, comment_state, import_state, inline_state } state
     82     = null_state, prev_state;
     83 
     84   prev_state = null_state;
     85 
     86   /* read from stdin if `-' is the filename.  */
     87   f = STREQ (filename, "-") ? stdin : fopen (filename, "r");
     88   if (!f)
     89     {
     90       error (_("%s: could not open --css-file: %s"), progname, filename);
     91       return NULL;
     92     }
     93 
     94   /* Read the file.  The @import statements must come at the beginning,
     95      with only whitespace and comments allowed before any inline css code.  */
     96   while ((c = getc (f)) >= 0)
     97     {
     98       if (c == '\n')
     99         lineno++;
    100 
    101       switch (state)
    102         {
    103         case null_state: /* between things */
    104           if (c == '@')
    105             { /* Only @import and @charset should switch into
    106                  import_state, other @-commands, such as @media, should
    107                  put us into inline_state.  I don't think any other css
    108                  @-commands start with `i' or `c', although of course
    109                  this will break when such a command is defined.  */
    110               int nextchar = getc (f);
    111               if (nextchar == 'i' || nextchar == 'c')
    112                 {
    113                   append_char (import_text, c);
    114                   state = import_state;
    115                 }
    116               else
    117                 {
    118                   ungetc (nextchar, f);  /* wasn't an @import */
    119                   state = inline_state;
    120                 }
    121             }
    122           else if (c == '/')
    123             { /* possible start of a comment */
    124               int nextchar = getc (f);
    125               if (nextchar == '*')
    126                 state = comment_state;
    127               else
    128                 {
    129                   ungetc (nextchar, f); /* wasn't a comment */
    130                   state = inline_state;
    131                 }
    132             }
    133           else if (isspace (c))
    134             ; /* skip whitespace; maybe should use c_isspace?  */
    135 
    136           else
    137             /* not an @import, not a comment, not whitespace: we must
    138                have started the inline text.  */
    139             state = inline_state;
    140 
    141           if (state == inline_state)
    142             append_char (inline_text, c);
    143 
    144           if (state != null_state)
    145             prev_state = null_state;
    146           break;
    147 
    148         case comment_state:
    149           if (c == '/' && lastchar == '*')
    150             state = prev_state;  /* end of comment */
    151           break;  /* else ignore this comment char */
    152 
    153         case import_state:
    154           append_char (import_text, c);  /* include this import char */
    155           if (c == ';')
    156             { /* done with @import */
    157               append_char (import_text, '\n');  /* make the output nice */
    158               state = null_state;
    159               prev_state = import_state;
    160             }
    161           break;
    162 
    163         case inline_state:
    164           /* No harm in writing out comments, so don't bother parsing
    165              them out, just append everything.  */
    166           append_char (inline_text, c);
    167           break;
    168         }
    169 
    170       lastchar = c;
    171     }
    172 
    173   /* Reached the end of the file.  We should not be still in a comment.  */
    174   if (state == comment_state)
    175     warning (_("%s:%d: --css-file ended in comment"), filename, lineno);
    176 
    177   /* Write the @import text, if any.  */
    178   if (import_text->buffer)
    179     {
    180       add_word (import_text->buffer);
    181       free (import_text->buffer);
    182       free (import_text);
    183     }
    184 
    185   /* We're wasting the buffer struct memory, but so what.  */
    186   return inline_text->buffer;
    187 }
    188 
    189 HSTACK *htmlstack = NULL;
    191 
    192 /* See html.h.  */
    193 int html_output_head_p = 0;
    194 int html_title_written = 0;
    195 
    196 void
    197 html_output_head (void)
    198 {
    199   static const char *html_title = NULL;
    200   char *encoding;
    201 
    202   if (html_output_head_p)
    203     return;
    204   html_output_head_p = 1;
    205 
    206   encoding = current_document_encoding ();
    207 
    208   /* The <title> should not have markup, so use text_expansion.  */
    209   if (!html_title)
    210     html_title = escape_string (title ?
    211         text_expansion (title) : (char *) _("Untitled"));
    212 
    213   /* Make sure this is the very first string of the output document.  */
    214   output_paragraph_offset = 0;
    215 
    216   add_html_block_elt_args ("<html lang=\"%s\">\n<head>\n",
    217       language_table[language_code].abbrev);
    218 
    219   /* When splitting, add current node's name to title if it's available and not
    220      Top.  */
    221   if (splitting && current_node && !STREQ (current_node, "Top"))
    222     add_word_args ("<title>%s - %s</title>\n",
    223         escape_string (xstrdup (current_node)), html_title);
    224   else
    225     add_word_args ("<title>%s</title>\n",  html_title);
    226 
    227   add_word ("<meta http-equiv=\"Content-Type\" content=\"text/html");
    228   if (encoding && *encoding)
    229     add_word_args ("; charset=%s", encoding);
    230 
    231   add_word ("\">\n");
    232 
    233   if (!document_description)
    234     document_description = html_title;
    235 
    236   add_word_args ("<meta name=\"description\" content=\"%s\">\n",
    237                  document_description);
    238   add_word_args ("<meta name=\"generator\" content=\"makeinfo %s\">\n",
    239                  VERSION);
    240 
    241   /* Navigation bar links.  */
    242   if (!splitting)
    243     add_word ("<link title=\"Top\" rel=\"top\" href=\"#Top\">\n");
    244   else if (tag_table)
    245     {
    246       /* Always put a top link.  */
    247       add_word ("<link title=\"Top\" rel=\"start\" href=\"index.html#Top\">\n");
    248 
    249       /* We already have a top link, avoid duplication.  */
    250       if (tag_table->up && !STREQ (tag_table->up, "Top"))
    251         add_link (tag_table->up, "rel=\"up\"");
    252 
    253       if (tag_table->prev)
    254         add_link (tag_table->prev, "rel=\"prev\"");
    255 
    256       if (tag_table->next)
    257         add_link (tag_table->next, "rel=\"next\"");
    258 
    259       /* fixxme: Look for a way to put links to various indices in the
    260          document.  Also possible candidates to be added here are First and
    261          Last links.  */
    262     }
    263   else
    264     {
    265       /* We are splitting, but we neither have a tag_table.  So this must be
    266          index.html.  So put a link to Top. */
    267       add_word ("<link title=\"Top\" rel=\"start\" href=\"#Top\">\n");
    268     }
    269 
    270   add_word ("<link href=\"http://www.gnu.org/software/texinfo/\" \
    271 rel=\"generator-home\" title=\"Texinfo Homepage\">\n");
    272 
    273   if (copying_text)
    274     { /* It is not ideal that we include the html markup here within
    275          <head>, so we use text_expansion.  */
    276       insert_string ("<!--\n");
    277       insert_string (text_expansion (copying_text));
    278       insert_string ("-->\n");
    279     }
    280 
    281   /* Put the style definitions in a comment for the sake of browsers
    282      that don't support <style>.  */
    283   add_word ("<meta http-equiv=\"Content-Style-Type\" content=\"text/css\">\n");
    284   add_word ("<style type=\"text/css\"><!--\n");
    285 
    286   {
    287     char *css_inline = NULL;
    288 
    289     if (css_include)
    290       /* This writes out any @import commands from the --css-file,
    291          and returns any actual css code following the imports.  */
    292       css_inline = process_css_file (css_include);
    293 
    294     /* This seems cleaner than adding <br>'s at the end of each line for
    295        these "roman" displays.  It's hardly the end of the world if the
    296        browser doesn't do <style>s, in any case; they'll just come out in
    297        typewriter.  */
    298 #define CSS_FONT_INHERIT "font-family:inherit"
    299     add_word_args ("  pre.display { %s }\n", CSS_FONT_INHERIT);
    300     add_word_args ("  pre.format  { %s }\n", CSS_FONT_INHERIT);
    301 
    302     /* Alternatively, we could do <font size=-1> in insertion.c, but this
    303        way makes it easier to override.  */
    304 #define CSS_FONT_SMALLER "font-size:smaller"
    305     add_word_args ("  pre.smalldisplay { %s; %s }\n", CSS_FONT_INHERIT,
    306                    CSS_FONT_SMALLER);
    307     add_word_args ("  pre.smallformat  { %s; %s }\n", CSS_FONT_INHERIT,
    308                    CSS_FONT_SMALLER);
    309     add_word_args ("  pre.smallexample { %s }\n", CSS_FONT_SMALLER);
    310     add_word_args ("  pre.smalllisp    { %s }\n", CSS_FONT_SMALLER);
    311 
    312     /* Since HTML doesn't have a sc element, we use span with a bit of
    313        CSS spice instead.  */
    314 #define CSS_FONT_SMALL_CAPS "font-variant:small-caps"
    315     add_word_args ("  span.sc    { %s }\n", CSS_FONT_SMALL_CAPS);
    316 
    317     /* Roman (default) font class, closest we can come.  */
    318 #define CSS_FONT_ROMAN "font-family:serif; font-weight:normal;"
    319     add_word_args ("  span.roman { %s } \n", CSS_FONT_ROMAN);
    320 
    321     /* Sans serif font class.  */
    322 #define CSS_FONT_SANSSERIF "font-family:sans-serif; font-weight:normal;"
    323     add_word_args ("  span.sansserif { %s } \n", CSS_FONT_SANSSERIF);
    324 
    325     /* Write out any css code from the user's --css-file.  */
    326     if (css_inline)
    327       insert_string (css_inline);
    328 
    329     add_word ("--></style>\n");
    330   }
    331 
    332   add_word ("</head>\n<body>\n");
    333 
    334   if (title && !html_title_written && titlepage_cmd_present)
    335     {
    336       add_word_args ("<h1 class=\"settitle\">%s</h1>\n", html_title);
    337       html_title_written = 1;
    338     }
    339 
    340   free (encoding);
    341 }
    342 
    343 /* Escape HTML special characters in the string if necessary,
    345    returning a pointer to a possibly newly-allocated one. */
    346 char *
    347 escape_string (char *string)
    348 {
    349   char *newstring;
    350   int i = 0, newlen = 0;
    351 
    352   do
    353     {
    354       /* Find how much to allocate. */
    355       switch (string[i])
    356         {
    357         case '"':
    358           newlen += 6;          /* `&quot;' */
    359           break;
    360         case '&':
    361           newlen += 5;          /* `&amp;' */
    362           break;
    363         case '<':
    364         case '>':
    365           newlen += 4;          /* `&lt;', `&gt;' */
    366           break;
    367         default:
    368           newlen++;
    369         }
    370     }
    371   while (string[i++]);
    372 
    373   if (newlen == i) return string; /* Already OK. */
    374 
    375   newstring = xmalloc (newlen);
    376   i = 0;
    377   do
    378     {
    379       switch (string[i])
    380         {
    381         case '"':
    382           strcpy (newstring, "&quot;");
    383           newstring += 6;
    384           break;
    385         case '&':
    386           strcpy (newstring, "&amp;");
    387           newstring += 5;
    388           break;
    389         case '<':
    390           strcpy (newstring, "&lt;");
    391           newstring += 4;
    392           break;
    393         case '>':
    394           strcpy (newstring, "&gt;");
    395           newstring += 4;
    396           break;
    397         default:
    398           newstring[0] = string[i];
    399           newstring++;
    400         }
    401     }
    402   while (string[i++]);
    403   free (string);
    404   return newstring - newlen;
    405 }
    406 
    407 /* Save current tag.  */
    409 static void
    410 push_tag (char *tag, char *attribs)
    411 {
    412   HSTACK *newstack = xmalloc (sizeof (HSTACK));
    413 
    414   newstack->tag = tag;
    415   newstack->attribs = xstrdup (attribs);
    416   newstack->next = htmlstack;
    417   htmlstack = newstack;
    418 }
    419 
    420 /* Get last tag.  */
    421 static void
    422 pop_tag (void)
    423 {
    424   HSTACK *tos = htmlstack;
    425 
    426   if (!tos)
    427     {
    428       line_error (_("[unexpected] no html tag to pop"));
    429       return;
    430     }
    431 
    432   free (htmlstack->attribs);
    433 
    434   htmlstack = htmlstack->next;
    435   free (tos);
    436 }
    437 
    438 /* Check if tag is an empty or a whitespace only element.
    439    If so, remove it, keeping whitespace intact.  */
    440 int
    441 rollback_empty_tag (char *tag)
    442 {
    443   int check_position = output_paragraph_offset;
    444   int taglen = strlen (tag);
    445   int rollback_happened = 0;
    446   char *contents = "";
    447   char *contents_canon_white = "";
    448 
    449   /* If output_paragraph is empty, we cannot rollback :-\  */
    450   if (output_paragraph_offset <= 0)
    451     return 0;
    452 
    453   /* Find the end of the previous tag.  */
    454   while (check_position > 0 && output_paragraph[check_position-1] != '>')
    455     check_position--;
    456 
    457   /* Save stuff between tag's end to output_paragraph's end.  */
    458   if (check_position != output_paragraph_offset)
    459     {
    460       contents = xmalloc (output_paragraph_offset - check_position + 1);
    461       memcpy (contents, output_paragraph + check_position,
    462           output_paragraph_offset - check_position);
    463 
    464       contents[output_paragraph_offset - check_position] = '\0';
    465 
    466       contents_canon_white = xstrdup (contents);
    467       canon_white (contents_canon_white);
    468     }
    469 
    470   /* Find the start of the previous tag.  */
    471   while (check_position > 0 && output_paragraph[check_position-1] != '<')
    472     check_position--;
    473 
    474   /* Check to see if this is the tag.  */
    475   if (strncmp ((char *) output_paragraph + check_position, tag, taglen) == 0
    476       && (whitespace (output_paragraph[check_position + taglen])
    477           || output_paragraph[check_position + taglen] == '>'))
    478     {
    479       if (!contents_canon_white || !*contents_canon_white)
    480         {
    481           /* Empty content after whitespace removal, so roll it back.  */
    482           output_paragraph_offset = check_position - 1;
    483           rollback_happened = 1;
    484 
    485           /* Original contents may not be empty (whitespace.)  */
    486           if (contents && *contents)
    487             {
    488               insert_string (contents);
    489               free (contents);
    490             }
    491         }
    492     }
    493 
    494   return rollback_happened;
    495 }
    496 
    497 /* Open or close TAG according to START_OR_END. */
    498 void
    499 #if defined (VA_FPRINTF) && __STDC__
    500 insert_html_tag_with_attribute (int start_or_end, char *tag, char *format, ...)
    501 #else
    502 insert_html_tag_with_attribute (start_or_end, tag, format, va_alist)
    503      int start_or_end;
    504      char *tag;
    505      char *format;
    506      va_dcl
    507 #endif
    508 {
    509   char *old_tag = NULL;
    510   char *old_attribs = NULL;
    511   char formatted_attribs[2000]; /* xx no fixed limits */
    512   int do_return = 0;
    513   extern int in_html_elt;
    514 
    515   if (start_or_end != START)
    516     pop_tag ();
    517 
    518   if (htmlstack)
    519     {
    520       old_tag = htmlstack->tag;
    521       old_attribs = htmlstack->attribs;
    522     }
    523 
    524   if (format)
    525     {
    526 #ifdef VA_SPRINTF
    527       va_list ap;
    528 #endif
    529 
    530       VA_START (ap, format);
    531 #ifdef VA_SPRINTF
    532       VA_SPRINTF (formatted_attribs, format, ap);
    533 #else
    534       sprintf (formatted_attribs, format, a1, a2, a3, a4, a5, a6, a7, a8);
    535 #endif
    536       va_end (ap);
    537     }
    538   else
    539     formatted_attribs[0] = '\0';
    540 
    541   /* Exception: can nest multiple spans.  */
    542   if (htmlstack
    543       && STREQ (htmlstack->tag, tag)
    544       && !(STREQ (tag, "span") && STREQ (old_attribs, formatted_attribs)))
    545     do_return = 1;
    546 
    547   if (start_or_end == START)
    548     push_tag (tag, formatted_attribs);
    549 
    550   if (do_return)
    551     return;
    552 
    553   in_html_elt++;
    554 
    555   /* texinfo.tex doesn't support more than one font attribute
    556      at the same time.  */
    557   if ((start_or_end == START) && old_tag && *old_tag
    558       && !rollback_empty_tag (old_tag))
    559     add_word_args ("</%s>", old_tag);
    560 
    561   if (*tag)
    562     {
    563       if (start_or_end == START)
    564         add_word_args (format ? "<%s %s>" : "<%s>", tag, formatted_attribs);
    565       else if (!rollback_empty_tag (tag))
    566         /* Insert close tag only if we didn't rollback,
    567            in which case the opening tag is removed.  */
    568         add_word_args ("</%s>", tag);
    569     }
    570 
    571   if ((start_or_end != START) && old_tag && *old_tag)
    572     add_word_args (strlen (old_attribs) > 0 ? "<%s %s>" : "<%s>",
    573         old_tag, old_attribs);
    574 
    575   in_html_elt--;
    576 }
    577 
    578 void
    579 insert_html_tag (int start_or_end, char *tag)
    580 {
    581   insert_html_tag_with_attribute (start_or_end, tag, NULL);
    582 }
    583 
    584 /* Output an HTML <link> to the filename for NODE, including the
    586    other string as extra attributes. */
    587 void
    588 add_link (char *nodename, char *attributes)
    589 {
    590   if (nodename)
    591     {
    592       add_html_elt ("<link ");
    593       add_word_args ("%s", attributes);
    594       add_word_args (" href=\"");
    595       add_anchor_name (nodename, 1);
    596       add_word_args ("\" title=\"%s\">\n", nodename);
    597     }
    598 }
    599 
    600 /* Output NAME with characters escaped as appropriate for an anchor
    601    name, i.e., escape URL special characters with our _00hh convention
    602    if OLD is zero.  (See the manual for details on the new scheme.)
    603 
    604    If OLD is nonzero, generate the node name with the 4.6-and-earlier
    605    convention of %hh (and more special characters output as-is, notably
    606    - and *).  This is only so that external references to old names can
    607    still work with HTML generated by the new makeinfo; the gcc folks
    608    needed this.  Our own HTML does not refer to these names.  */
    609 
    610 void
    611 add_escaped_anchor_name (char *name, int old)
    612 {
    613   canon_white (name);
    614 
    615   if (!old && !strchr ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
    616                        *name))
    617     { /* XHTML does not allow anything but an ASCII letter to start an
    618          identifier.  Therefore kludge in this constant string if we
    619          have a nonletter.  */
    620       add_word ("g_t");
    621     }
    622 
    623   for (; *name; name++)
    624     {
    625       if (cr_or_whitespace (*name))
    626         add_char ('-');
    627 
    628       else if (!old && !URL_SAFE_CHAR (*name))
    629         /* Cast so characters with the high bit set are treated as >128,
    630            for example o-umlaut should be 246, not -10.  */
    631         add_word_args ("_00%x", (unsigned char) *name);
    632 
    633       else if (old && !URL_SAFE_CHAR (*name) && !OLD_URL_SAFE_CHAR (*name))
    634         /* Different output convention, but still cast as above.  */
    635         add_word_args ("%%%x", (unsigned char) *name);
    636 
    637       else
    638         add_char (*name);
    639     }
    640 }
    641 
    642 /* Insert the text for the name of a reference in an HTML anchor
    643    appropriate for NODENAME.
    644 
    645    If HREF is zero, generate text for name= in the new node name
    646      conversion convention.
    647    If HREF is negative, generate text for name= in the old convention.
    648    If HREF is positive, generate the name for an href= attribute, i.e.,
    649      including the `#' if it's an internal reference.   */
    650 void
    651 add_anchor_name (char *nodename, int href)
    652 {
    653   if (href > 0)
    654     {
    655       if (splitting)
    656 	add_url_name (nodename, href);
    657       add_char ('#');
    658     }
    659   /* Always add NODENAME, so that the reference would pinpoint the
    660      exact node on its file.  This is so several nodes could share the
    661      same file, in case of file-name clashes, but also for more
    662      accurate browser positioning.  */
    663   if (strcasecmp (nodename, "(dir)") == 0)
    664     /* Strip the parens, but keep the original letter-case.  */
    665     add_word_args ("%.3s", nodename + 1);
    666   else if (strcasecmp (nodename, "top") == 0)
    667     add_word ("Top");
    668   else
    669     add_escaped_anchor_name (nodename, href < 0);
    670 }
    671 
    672 /* Insert the text for the name of a reference in an HTML url, aprropriate
    673    for NODENAME */
    674 void
    675 add_url_name (char *nodename, int href)
    676 {
    677     add_nodename_to_filename (nodename, href);
    678 }
    679 
    680 /* Convert non [A-Za-z0-9] to _00xx, where xx means the hexadecimal
    681    representation of the ASCII character.  Also convert spaces and
    682    newlines to dashes.  */
    683 static void
    684 fix_filename (char *filename)
    685 {
    686   int i;
    687   int len = strlen (filename);
    688   char *oldname = xstrdup (filename);
    689 
    690   *filename = '\0';
    691 
    692   for (i = 0; i < len; i++)
    693     {
    694       if (cr_or_whitespace (oldname[i]))
    695         strcat (filename, "-");
    696       else if (URL_SAFE_CHAR (oldname[i]))
    697         strncat (filename, (char *) oldname + i, 1);
    698       else
    699         {
    700           char *hexchar = xmalloc (6 * sizeof (char));
    701           sprintf (hexchar, "_00%x", (unsigned char) oldname[i]);
    702           strcat (filename, hexchar);
    703           free (hexchar);
    704         }
    705 
    706       /* Check if we are nearing boundaries.  */
    707       if (strlen (filename) >= PATH_MAX - 20)
    708         break;
    709     }
    710 
    711   free (oldname);
    712 }
    713 
    714 /* As we can't look-up a (forward-referenced) nodes' html filename
    715    from the tentry, we take the easy way out.  We assume that
    716    nodenames are unique, and generate the html filename from the
    717    nodename, that's always known.  */
    718 static char *
    719 nodename_to_filename_1 (char *nodename, int href)
    720 {
    721   char *p;
    722   char *filename;
    723   char dirname[PATH_MAX];
    724 
    725   if (strcasecmp (nodename, "Top") == 0)
    726     {
    727       /* We want to convert references to the Top node into
    728 	 "index.html#Top".  */
    729       if (href)
    730 	filename = xstrdup ("index.html"); /* "#Top" is added by our callers */
    731       else
    732 	filename = xstrdup ("Top");
    733     }
    734   else if (strcasecmp (nodename, "(dir)") == 0)
    735     /* We want to convert references to the (dir) node into
    736        "../index.html".  */
    737     filename = xstrdup ("../index.html");
    738   else
    739     {
    740       filename = xmalloc (PATH_MAX);
    741       dirname[0] = '\0';
    742       *filename = '\0';
    743 
    744       /* Check for external reference: ``(info-document)node-name''
    745 	 Assume this node lives at: ``../info-document/node-name.html''
    746 
    747 	 We need to handle the special case (sigh): ``(info-document)'',
    748 	 ie, an external top-node, which should translate to:
    749 	 ``../info-document/info-document.html'' */
    750 
    751       p = nodename;
    752       if (*nodename == '(')
    753 	{
    754 	  int length;
    755 
    756 	  p = strchr (nodename, ')');
    757 	  if (p == NULL)
    758 	    {
    759 	      line_error (_("[unexpected] invalid node name: `%s'"), nodename);
    760 	      xexit (1);
    761 	    }
    762 
    763 	  length = p - nodename - 1;
    764 	  if (length > 5 &&
    765 	      FILENAME_CMPN (p - 5, ".info", 5) == 0)
    766 	    length -= 5;
    767 	  /* This is for DOS, and also for Windows and GNU/Linux
    768 	     systems that might have Info files copied from a DOS 8+3
    769 	     filesystem.  */
    770 	  if (length > 4 &&
    771 	      FILENAME_CMPN (p - 4, ".inf", 4) == 0)
    772 	    length -= 4;
    773 	  strcpy (filename, "../");
    774 	  strncpy (dirname, nodename + 1, length);
    775 	  *(dirname + length) = '\0';
    776 	  fix_filename (dirname);
    777 	  strcat (filename, dirname);
    778 	  strcat (filename, "/");
    779 	  p++;
    780 	}
    781 
    782       /* In the case of just (info-document), there will be nothing
    783 	 remaining, and we will refer to ../info-document/, which will
    784 	 work fine.  */
    785       strcat (filename, p);
    786       if (*p)
    787 	{
    788 	  /* Hmm */
    789 	  fix_filename (filename + strlen (filename) - strlen (p));
    790 	  strcat (filename, ".html");
    791 	}
    792     }
    793 
    794   /* Produce a file name suitable for the underlying filesystem.  */
    795   normalize_filename (filename);
    796 
    797 #if 0
    798   /* We add ``#Nodified-filename'' anchor to external references to be
    799      prepared for non-split HTML support.  Maybe drop this. */
    800   if (href && *dirname)
    801     {
    802       strcat (filename, "#");
    803       strcat (filename, p);
    804       /* Hmm, again */
    805       fix_filename (filename + strlen (filename) - strlen (p));
    806     }
    807 #endif
    808 
    809   return filename;
    810 }
    811 
    812 /* If necessary, ie, if current filename != filename of node, output
    813    the node name.  */
    814 void
    815 add_nodename_to_filename (char *nodename, int href)
    816 {
    817   /* for now, don't check: always output filename */
    818   char *filename = nodename_to_filename_1 (nodename, href);
    819   add_word (filename);
    820   free (filename);
    821 }
    822 
    823 char *
    824 nodename_to_filename (char *nodename)
    825 {
    826   /* The callers of nodename_to_filename use the result to produce
    827      <a href=, so call nodename_to_filename_1 with last arg non-zero.  */
    828   return nodename_to_filename_1 (nodename, 1);
    829 }
    830