# $Id$ $VERSION{''.__FILE__} = '$Revision$'; # # >>Title:: HTML Format Driver # # >>Copyright:: # Copyright (c) 1992-1999, Ian Clatworthy (ianc@mincom.com). # You may distribute under the terms specified in the LICENSE file. # # >>History:: # ----------------------------------------------------------------------- # Date Who Change # 29-Feb-96 ianc SDF 2.000 # ----------------------------------------------------------------------- # # >>Purpose:: # This library provides an [[SDF_DRIVER]] which generates # [[HTML]] files. # # >>Description:: # # >>Limitations:: # Lists which have ordered items, then unordered items, then # ordered items all at the same level are output as three # separate lists. As a result, the numbering in the third list # restarts even if you don't want it to. # # After hypertext jumps have been added throughout a paragraph, # we should go back over the paragraph and unnest any nested jumps. # # >>Resources:: # # >>Implementation:: # ##### Constants ##### # Mapping table for characters %_HTML_CHAR = ( 'bullet', '.', 'c', '© ', 'cent', '¢ ', 'dagger', '^', 'doubledagger', '#', 'emdash', '--', 'endash', '-', 'emspace', '  ', 'enspace', '  ', 'lbrace', '{', 'lbracket', '[', 'nbdash', '-', 'nbspace', '  ', 'nl', '
', 'pound', '£ ', 'r', '® ', 'rbrace', '}', 'rbracket', ']', 'tab', ' ', 'tm', '™ ', # not sure about this 'yen', '¥ ', ); # Directive mapping table %_HTML_HANDLER = ( 'tuning', '_HtmlHandlerTuning', 'endtuning', '_HtmlHandlerEndTuning', 'table', '_HtmlHandlerTable', 'row', '_HtmlHandlerRow', 'cell', '_HtmlHandlerCell', 'endtable', '_HtmlHandlerEndTable', 'import', '_HtmlHandlerImport', 'inline', '_HtmlHandlerInline', 'output', '_HtmlHandlerOutput', 'object', '_HtmlHandlerObject', 'stylesheet', '_HtmlHandlerStyleSheet', 'div', '_HtmlHandlerDiv', 'enddiv', '_HtmlHandlerEndDiv', ); # Phrase directive mapping table %_HTML_PHRASE_HANDLER = ( 'char', '_HtmlPhraseHandlerChar', 'import', '_HtmlPhraseHandlerImport', 'inline', '_HtmlPhraseHandlerInline', 'variable', '_HtmlPhraseHandlerVariable', ); # Table states $_HTML_INTABLE = 1; $_HTML_INROW = 2; $_HTML_INCELL = 3; # Attribute types - this is used to decide if an attribute is legal, # and if it is, whether to quote the value (string) or not. # 'class' types get made into a class attribute. %_HTML_ATTR_TYPES = ( 'align', 'string', 'alt', 'string', 'border', 'integer', 'class', 'string', 'changed', 'class', ); # Attributes mapping to styles %_HTML_STYLE_MAP = ( 'family', 'font-family: %s', 'size', 'font-size: %s', 'bold', 'font-weight: bold', 'italics', 'font-style: italic', 'underline','text-decoration: underline', 'color', 'color: %s', 'bgcolor', 'background-color: %s', # 'align', 'text-align: %s', 'first', 'text-indent: %s', 'left', 'margin-left: %s', 'right', 'margin-right: %s', ); ##### Variables ##### # Table/cell states @_html_tbl_state = (); @_html_tbl_endtokens = (); @_html_tbl_previndent = (); $_html_cell_paracnt = (); # Stack of topic file offsets and filenames @_html_topic_offset = (); @_html_topic_file = (); # Current topic and level $_html_topic = ''; $_html_topic_level = 0; # File/text combinations which start a new topic %_html_topic_start = (); # File/text lookup to a jump target %_html_jump_id = (); ## Ordered list state #$_html_in_olist = 0; # Topic counter for building derived topic names $_html_topic_cntr = 0; # Meta information for this file @_html_meta = (); # Links this file @_html_links = (); # Stylesheet for this file @_html_stylesheet = (); # Counts for each class attribute %_html_class_count = (); # Division stack (contents is the name (i.e. class) of opened divisions) @_html_divs = (); # Title division text @_html_title_div = (); ##### Routines ##### # # >>Description:: # {{Y:HtmlFormat}} is an SDF driver which outputs HTML. # sub HtmlFormat { local(*data) = @_; local(@result); local(@contents); local(@data2, @contents2, %var2, @result2); local($msg_cursor, %msg_counts); local($main); local(@topics_table, @jumps_table); # Init global data $_html_topic = ''; $_html_topic_level = 0; $_html_topic_cntr = 0; %_html_topic_start = (); %_html_jump_id = (); @_html_meta = (); @_html_links = (); @_html_stylesheet = (); %_html_class_count = (); @_html_divs = (); @_html_title_div = (); # If we're building topics, save the data for a second pass later if ($SDF_USER'var{'HTML_TOPICS_MODE'}) { @data2 = @data; # Get the current message cursor - we skip the second pass # if errors are found $msg_cursor = &AppMsgNextIndex(); } # Format the paragraphs @contents = (); @result = &_HtmlFormatSection(*data, *contents); # Save away any unclosed topics while (@_html_topic_file) { &_HtmlHandlerOutput(*result, '-'); } # Build the final result. ## Note that we must do this AFTER the subtopics stuff in order ## to get the next/previous topic data needed for the default ## header/footer. @result = &_HtmlFinalise(*result, *contents); # If there were no problems in the first pass, # build the sub-topics, if requested %msg_counts = &AppMsgCounts($msg_cursor); if ($msg_counts{'error'} || $msg_counts{'abort'} || $msg_counts{'fatal'} ) { # do nothing } elsif ($SDF_USER'var{'HTML_TOPICS_MODE'}) { $main = $SDF_USER'var{'DOC_BASE'}; @topics_table = (); @jumps_table = (); &_HtmlBuildTopicsData($main, *topics_table, *jumps_table); # Save the topics and jump data, so users can (eventually) rebuild # just a single topic. if ($SDF_USER'var{'HTML_SDJ'}) { &_HtmlSaveTopicsData($main, *topics_table, *jumps_table); } # Initialise things ready for the next pass %var2 = %convert_var; # get the original set of variables $var2{'HTML_MAIN_TITLE'} = $SDF_USER'var{'DOC_TITLE'}; $var2{'HTML_URL_CONTENTS'} = $SDF_USER'var{'DOC_BASE'} . ".html"; $var2{'HTML_TOPICS_MODE'} = 0; $var2{'HTML_SUBTOPICS_MODE'} = 1; &SdfInit(*var2); &SDF_USER'topics_Filter(*topics_table, 'data', 1); &SDF_USER'jumps_Filter(*jumps_table, 'data', 1); # Build the sub-topics @contents2 = (); #printf "DATA2:\n%s\nENDDATA2\n", join("\n", @data2); @result2 = &_HtmlFormatSection(*data2, *contents2); # Save away any unclosed topics while (@_html_topic_file) { &_HtmlHandlerOutput(*result2, '-'); } } # Return the result return @result; } # # >>_Description:: # {{Y:_HtmlBuildTopicsData}} builds the topics data # needed for sub-topic building. # sub _HtmlBuildTopicsData { local($main, *topics_table, *jumps_table) = @_; # local(); local($topic, $level, $label, $next, $prev, $up, %last_at); local($jump, $physical); # Ensure that the main topic is first and that it has the highest level #if ($SDF_USER'topics[0] eq $main) { # $SDF_USER'levels[0] = 0; #} #else { # unshift(@SDF_USER'topics, pop(@SDF_USER'topics)); # pop(@SDF_USER'levels); # unshift(@SDF_USER'levels, 0); #} unshift(@SDF_USER'topics, $main); unshift(@SDF_USER'levels, 0); # Build the topics table @topics_table = ("Topic|Label|Level|Next|Prev|Up"); $prev = $SDF_USER'topics[$#SDF_USER'topics]; %last_at = (); for ($i = 0; $i <= $#SDF_USER'topics; $i++) { $topic = $SDF_USER'topics[$i]; $level = $SDF_USER'levels[$i]; $label = $SDF_USER'topic_label{$topic}; $next = $i < $#SDF_USER'topics ? $SDF_USER'topics[$i + 1] : $SDF_USER'topics[0]; $up = $last_at{$level - 1}; push(@topics_table, "$topic|$label|$level|$next|$prev|$up"); # Save state for later iterations $prev = $topic; $last_at{$level} = $topic; } # Build the jumps table @jumps_table = ("Jump|Physical"); for $jump (sort keys %SDF_USER'jump) { $physical = $SDF_USER'jump{$jump}; push(@jumps_table, "$jump|$physical"); } } # # >>_Description:: # {{Y:_HtmlSaveTopicsData}} dumps topic and jump data to a file. # sub _HtmlSaveTopicsData { local($main, *topics_table, *jumps_table) = @_; # local(); local($file); # Save the topic and jump data $file = &NameJoin('', $main, 'sdj'); unless (open(SDM, ">$file")) { &AppMsg("warning", "unable to update topics file '$file'"); } else { # Output a warning message at the top print SDM "# WARNING: This file is automatically generated\n"; print SDM "# by SDF, so any changes you make will be lost!\n"; # Dump the topics data print SDM "\n"; print SDM "!block topics; data\n"; print SDM join("\n", @topics_table), "\n"; print SDM "!endblock\n"; # Dump the jumps data print SDM "\n"; print SDM "!block jumps\n"; print SDM join("\n", @jumps_table), "\n"; print SDM "!endblock\n"; # Close the file close(SDM); } } # # >>_Description:: # {{Y:_HtmlFormatSection}} formats a set of SDF paragraphs into HTML. # If a parameter is passed to contents, then that array is populated # with a generated Table of Contents. If {{division}} is set, the # result is placed in a DIV element with that class. # sub _HtmlFormatSection { local(*data, *contents, $division) = @_; local(@result); local($prev_tag, $prev_indent); local($para_tag, $para_text, %para_attrs); local($directive); ## Reset the ordered list state. I'm not absolutely sure that ## this is the best place to do this, but TJH had it here ## and I trust him (most of the time :-) #$_html_in_olist = 0; # Process the paragraphs @result = $division eq '' ? () : ("
"); $prev_tag = ''; $prev_indent = ''; while (($para_text, $para_tag, %para_attrs) = &SdfNextPara(*data)) { # handle directives if ($para_tag =~ /^__(\w+)$/) { $directive = $_HTML_HANDLER{$1}; if (defined &$directive) { &$directive(*result, $para_text, %para_attrs); } else { &AppTrace("html", 5, "ignoring internal directive '$1'"); } next; } # Add the paragraph &_HtmlParaAdd(*result, $para_tag, $para_text, *para_attrs, $prev_tag, $prev_indent, *contents); } # Do this stuff before starting next loop iteration continue { $prev_tag = $para_tag; $prev_indent = $para_attrs{'in'}; } ## Filter out the dummy tag we use to get lists right #for ($i = 0; $i < $#result; $i++) { # $result[$i] =~ s###g; #} # Close off the DIV element, if any push(@result, "
") if $division ne ''; # Return result return @result; } # # >>_Description:: # {{Y:_HtmlParaAdd}} adds a paragraph. # sub _HtmlParaAdd { local(*result, $para_tag, $para_text, *para_attrs, $prev_tag, $prev_indent, *contents) = @_; # local(); local($is_example); local($para_fmt); local($para_override); local($para); local($hdg_level); local($toc_jump); local($label); local($indent); local($list_tag); local($note_line_width); local($note_attrs); # Get the example flag $is_example = $SDF_USER'parastyles_category{$para_tag} eq 'example'; # Enumerated lists are the same as list paragraphs at the previous level, # except that we bold the text if ($para_tag =~ /^LI(\d)$/) { $para_tag = $1 > 1 ? "L" . ($1 - 1) : 'N'; $para_attrs{'bold'} = 1; } # Get the target format name $para_fmt = $SDF_USER'parastyles_to{$para_tag}; $para_fmt = $is_example ? 'PRE' : 'P' if $para_fmt eq ''; # Map the attributes &SdfAttrMap(*para_attrs, 'html', *SDF_USER'paraattrs_to, *SDF_USER'paraattrs_map, *SDF_USER'paraattrs_attrs, $SDF_USER'parastyles_attrs{$para_tag}); # Build the Table of Contents as we go $toc_jump = ''; if ($para_tag =~ /^([HAP])(\d)$/) { $hdg_level = $2; my $orig_para_text = $para_text; $para_text = &SdfHeadingPrefix($1, $2) . $para_text; if ($SDF_USER'var{'HTML_SUBTOPICS_MODE'}) { $para_fmt = "H" . substr($para_attrs{'orig_style'}, 1); } else { $para_fmt = "H" . $hdg_level; } if ($hdg_level <= $SDF_USER'var{'DOC_TOC'} && !$para_attrs{'notoc'}) { # Build a plain list in SDF. If we're building topics and we're # building the contents, make sure the jumps go to the right spot. if ($SDF_USER'var{'HTML_TOPICS_MODE'}) { #$toc_jump = &NameJoin('', $SDF_USER'var{'FILE_BASE'}, "html"); #if ($SDF_USER'topic_label{$SDF_USER'var{'FILE_BASE'}} ne $para_text) { # $toc_jump .= "#" . $para_attrs{'id'}; #} $toc_jump = $_html_jump_id{$SDF_USER'var{'FILE_BASE'},$orig_para_text}; } else { $toc_jump = "#" . $para_attrs{'id'}; $toc_jump = "#HDR" . ($#contents + 1) if $toc_jump eq '#'; } #$toc_jump =~ s/(['\\])/\\$1/g; #push(@contents, "L${hdg_level}" . "[jump='$toc_jump']$para_text"); push(@contents, &SdfJoin("L${hdg_level}", $para_text, "jump", $toc_jump)); } } # Handle lists elsif ($para_tag =~ /^(L[FUN]?)(\d)$/) { $para_attrs{'in'} = $2; if ($1 eq 'LU') { $para_fmt = 'UL'; } elsif ($1 eq 'L') { $para_fmt = 'UL PLAIN'; } else { $para_fmt = 'OL'; } } # Handle user-defined formatting if ($para_attrs{'out_style'}) { $para_fmt = $para_attrs{'out_style'}; delete $para_attrs{'out_style'}; } # Prepend the label, if any (replacing tabs with spaces) $label = $para_attrs{'label'}; $label = 'Note: ' if ($para_tag eq 'Note' || $para_tag eq 'NB') && $label eq ''; $label =~ s/\\t/ /g; $para_text = "{{2:$label}}$para_text" if $label ne ''; # Indent examples, if necessary if ($is_example && $para_attrs{'in'}) { $para_text = " " x ($para_attrs{'in'} * 4) . $para_text; delete $para_attrs{'in'}; } # Format the paragraph body if ($para_attrs{'verbatim'}) { $para = &_HtmlEscape($para_text); delete $para_attrs{'verbatim'}; } else { $para = &_HtmlParaText($para_text); } # Add surrounding lines for a note $note_line_width = 80; # Hard-coded for now $note_attrs = "WIDTH=\"$note_line_width%\" ALIGN=\"Left\""; if ($para_tag eq 'Note') { $para = "
\n$para\n
"; } elsif ($para_tag eq 'NB') { $para = "
\n$para"; } elsif ($para_tag eq 'NE') { $para = "
"; } # Empty cells look ugly so the hack below # puts a space in empty paragraphs inside cells. # Unfortunately, this means truly empty paragraphs # inside cells are not handled. Is this an issue? $para = ' ' if $para eq '' && @_html_tbl_state; ## Examples with change bars currently come out as separate ## paragraphs - this fixes the problem, for now #delete $para_attrs{'changed'} if $para_attrs{'changed'}; # Build result $indent = $para_attrs{'in'}; #if ($is_example && $para_tag eq $prev_tag && !%para_attrs) { if ($is_example && $para_tag eq $prev_tag) { &_HtmlParaAppend(*result, $para); } elsif ($indent && $prev_indent != 0) { $item = &_HtmlElement($para_fmt, $para, %para_attrs); &_HtmlItemAppend(*result, $item, $indent, $prev_indent, $para_tag, $prev_tag, *para_attrs); } # If the first paragraph inside a table cell is a plain paragraph, # then we do not surrounded it by

and

as # Netscape then outputs too much whitespace. elsif (@_html_tbl_state && $_html_cell_paracnt++ == 0 && $para_fmt eq 'P') { push(@result, $para); } else { $para = &_HtmlElement($para_fmt, $para, %para_attrs); # Handle lists which begin at an indent greater than 1 $list_tag = substr($para, 1, 2) if $indent; while (--$indent > 0) { $para = "<$list_tag>$para"; } # Prepend the table of contents jump id, if necessary if ($toc_jump =~ /^#HDR\d+$/) { $para = " \n$para"; } push(@result, $para); } } # # >>_Description:: # {{Y:_HtmlParaText}} converts SDF paragraph text into HTML. # sub _HtmlParaText { local($para_text) = @_; local($para); local($state); local($sect_type, $char_tag, $text, %sect_attrs); local($url); local($added_anchors); local(@char_fonts); local($char_font); local($directive); local($char_attrs); # Process the text $para = ''; $state = 0; while (($sect_type, $text, $char_tag, %sect_attrs) = &SdfNextSection(*para_text, *state)) { # Build the paragraph if ($sect_type eq 'string') { $para .= &_HtmlEscape($text); } elsif ($sect_type eq 'phrase') { # Expand out link phrases if ($char_tag eq 'L') { ($text, $url) = &SDF_USER'ExpandLink($text); $sect_attrs{'jump'} = $url; } # Escape any special characters $text = &_HtmlEscape($text); # Expand non-breaking spaces, if necessary if ($char_tag eq 'S') { $text =~ s/ / /g; } # Empty cells look ugly so the hack below # puts a space in empty phrases inside cells. # Unfortunately, this means truly empty phrases # inside cells are not handled. Is this an issue? $text = ' ' if $text eq '' && @_html_tbl_state; # If this is a jump, ignore the style (i.e. make it 'as-is') #$char_tag = 'A' if $sect_attrs{'jump'} ne ''; # Add hypertext stuff $added_anchors = &_HtmlAddAnchors(*text, *sect_attrs); # Process formatting attributes &SdfAttrMap(*sect_attrs, 'html', *SDF_USER'phraseattrs_to, *SDF_USER'phraseattrs_map, *SDF_USER'phraseattrs_attrs, $SDF_USER'phrasestyles_attrs{$char_tag}); $char_attrs = &_HtmlAttr(*sect_attrs); #print STDERR "char_attrs is $char_attrs<\n"; # Map the font $char_font = $SDF_USER'phrasestyles_to{$char_tag}; $char_font = $char_tag if $char_font eq '' && !$added_anchors; # If attributes are specified for an SDF font, use a SPAN if ($char_font =~ /^SDF/ && $char_attrs ne '') { $char_font = 'SPAN'; } # Add the text for this phrase push(@char_fonts, $char_font); if ($char_font ne '' && $char_font !~ /^SDF/) { $para .= "<$char_font$char_attrs>$text"; } else { $para .= $text; } } elsif ($sect_type eq 'phrase_end') { $char_font = pop(@char_fonts); $para .= "" if $char_font ne '' && $char_font !~ /^SDF/; } elsif ($sect_type eq 'special') { $directive = $_HTML_PHRASE_HANDLER{$char_tag}; if (defined &$directive) { &$directive(*para, $text, %sect_attrs); } else { &AppMsg("warning", "ignoring special phrase '$1' in HTML driver"); } } else { &AppMsg("warning", "unknown section type '$sect_type' in HTML driver"); } } # Return result return $para; } # # >>_Description:: # {{Y:_HtmlFinalise}} generates the final HTML file. # sub _HtmlFinalise { local(*body, *contents) = @_; # local(@result); local($title, @sdf_title, @title); local($version, @head); local($body); # Build the BODY opening stuff $body = "BODY"; $body .= sprintf(' BACKGROUND="%s"', $SDF_USER'var{"HTML_BG_IMAGE"}) if defined($SDF_USER'var{"HTML_BG_IMAGE"}); $body .= sprintf(' BGPROPERTIES="FIXED"') if $SDF_USER'var{"HTML_BG_FIXED"}; $body .= sprintf(' BGCOLOR="%s"', $SDF_USER'var{"HTML_BG_COLOR"}) if defined($SDF_USER'var{"HTML_BG_COLOR"}); $body .= sprintf(' TEXT="%s"', $SDF_USER'var{"HTML_TEXT_COLOR"}) if defined($SDF_USER'var{"HTML_TEXT_COLOR"}); $body .= sprintf(' LINK="%s"', $SDF_USER'var{"HTML_LINK_COLOR"}) if defined($SDF_USER'var{"HTML_LINK_COLOR"}); $body .= sprintf(' VLINK="%s"', $SDF_USER'var{"HTML_VLINK_COLOR"}) if defined($SDF_USER'var{"HTML_VLINK_COLOR"}); # Convert the title, if any, to HTML $title = $SDF_USER'var{'HTML_TITLE'}; $title = $SDF_USER'var{'DOC_TITLE'} if !defined($title); if ($title) { @sdf_title = ("TITLE:$title"); @title = &_HtmlFormatSection(*sdf_title, *dummy); } else { @title = (); } # Prepend some useful things to the stylesheet, if applicable if ($_html_class_count{'changed'}) { my $changed_color = $SDF_USER'var{'HTML_CHANGED_COLOR'}; unshift(@_html_stylesheet, ".changed {background-color: $changed_color}"); } # Build the HEAD element (and append BODY opening) $version = $SDF_USER'var{'SDF_VERSION'}; @head = ( '', '', '', '', "', '', '', ); push(@head, @title) if @title; push(@head, @_html_meta) if @_html_meta; push(@head, @_html_links) if @_html_links; if (@_html_stylesheet) { push(@head, '', ); } push(@head, '', "<$body>", ''); # Build the body contents, unless we're generating an input file for # the HTMLDOC package unless ($SDF_USER'var{'HTMLDOC'}) { &_HtmlFinaliseBodyContents(*body, *contents); } # Return result push(@body, '', '', ''); return (@head, @body); } # # >>_Description:: # {{Y:_HtmlFinaliseBodyContents}} generates the final BODY contents. # sub _HtmlFinaliseBodyContents { local(*body, *contents) = @_; # local(@result); local($macro, @header, @footer); local(@dummy); local(@html_contents); #local($rec, $toc_posn); # Wrap the main body in a main division unshift(@body, '
'); push(@body, '
'); # Prepend the Table of Contents, if any if (@contents) { # Finish formatting the table of contents # Note: we use a filter so that experts can override things! &SDF_USER'toc_html_Filter(*contents); # Now convert it to HTML @html_contents = &_HtmlFormatSection(*contents, *dummy, 'contents'); # If this is a MAIN document, make the body the contents # (i.e. ditch the contents). Otherwise, prepend it. if ($SDF_USER'var{'HTML_TOPICS_MODE'}) { @body = @html_contents; } else { unshift(@body, join("\n", @html_contents)); } } # If this is not a topic, prepend the title division, if any unless ($SDF_USER'var{'HTML_SUBTOPICS_MODE'}) { unshift(@body, @_html_title_div); } # Convert the header, if any, to HTML $macro = 'HTML_HEADER'; if ($SDF_USER'var{'HTML_SUBTOPICS_MODE'} && $SDF_USER'macro{'HTML_TOPIC_HEADER'}) { $macro = 'HTML_TOPIC_HEADER'; } if ($SDF_USER'macro{$macro} ne '') { @header = ("!$macro"); unshift(@body, &_HtmlFormatSection(*header, *dummy, 'header')); } # Convert the footer, if any, to HTML $macro = 'HTML_FOOTER'; if ($SDF_USER'var{'HTML_SUBTOPICS_MODE'} && $SDF_USER'macro{'HTML_TOPIC_FOOTER'}) { $macro = 'HTML_TOPIC_FOOTER'; } if ($SDF_USER'macro{$macro} ne '') { @footer = ("!$macro"); push(@body, &_HtmlFormatSection(*footer, *dummy, 'footer')); } # Add the pre-header and post-footer, if any my $pre_header = $SDF_USER'var{'HTML_PRE_HEADER'}; unshift(@body, $pre_header) if $pre_header ne ''; my $post_footer = $SDF_USER'var{'HTML_POST_FOOTER'}; push(@body, $post_footer) if $post_footer ne ''; } # # >>_Description:: # {{Y:_HtmlEscape}} escapes special symbols in HTML text. # sub _HtmlEscape { local($text) = @_; # local($result); local($old_match_flag); # Enable multi-line matching $old_match_flag = $*; $* = 1; # Escape the symbols $text =~ s/\&/&/g; $text =~ s/\/>/g; $text =~ s/\"/"/g; # Reset multi-line matching flag $* = $old_match_flag; # Return result $text; } # # >>_Description:: # {{Y:_HtmlAttr}} formats a set of attributes into HTML. # sub _HtmlAttr { local(*attrs) = @_; local($html); local($attr, $value, $type); local($style); local($style_map); # get the specified style info, if any $style = $attrs{'style'}; delete $attr{'style'}; for $attr (sort keys %attrs) { # get the attribute value $value = $attrs{$attr}; # get the attribute type & map to style info if ($attr =~ s/^html\.//) { $type = $_HTML_ATTR_TYPES{$attr}; $type = "string" if $type eq ''; } else { $type = $_HTML_ATTR_TYPES{$attr}; #print STDERR "attr: $attr, type: $type<\n"; } $style_map = $_HTML_STYLE_MAP{$attr}; if ($style_map ne '') { $style .= '; ' if $style ne ''; # if ($attr eq 'align') { # if ($value eq '1' || $value eq 'Full') { # $value = 'justify'; # } # else { # $value = "\l$value"; # } # } $style .= sprintf($style_map, $value); } next unless $type; # build the result (using uppercase attribute names) if ($type eq 'string') { $attr =~ tr/a-z/A-Z/; $value = &_HtmlEscape($value); $html .= " $attr=\"" . $value . '"'; } elsif ($type eq 'class') { $value = $attr; $attr = 'CLASS'; $html .= " $attr=\"" . $value . '"'; } else { $attr =~ tr/a-z/A-Z/; $html .= " $attr=$value"; } # Keep some stats on classes used so we can conditionally # add things to the stylesheet later $_html_class_count{$value}++ if $attr eq 'CLASS'; } # Add the style info, if any if ($style ne '') { $html .= " STYLE=\"" . &_HtmlEscape($style) . '"'; } # Return result $html; } # # >>_Description:: # {{Y:_HtmlElement}} formats a HTML element from a # tag, text and set of attributes. # sub _HtmlElement { local($tag, $text, %attr) = @_; # local($html); # For preformatted sections, tags go on separate lines $text = "\n$text\n" if $tag eq 'PRE'; # Add hypertext stuff &_HtmlAddAnchors(*text, *attr); # Bold the text, if requested if ($attr{'bold'}) { $text = "$text"; } # For list items, add the item stuff #$text = "\n
  • $text" if $tag =~ /^[UOx]L$/; $text = "\n
  • $text" if $tag =~ /^[UO]L$/; if ($tag eq 'UL PLAIN') { $tag = 'UL'; $text = "\n$text"; } # Return result if ($tag eq 'HR') { return "<$tag>$text"; } else { return "<$tag" . &_HtmlAttr(*attr) . ">$text"; } } # # >>_Description:: # {{Y:_HtmlAddAnchors}} adds hypertext jumps and ids to a section of text. # of text. It returns true if anchors were added. # sub _HtmlAddAnchors { local(*text, *attr) = @_; local($result); local($value); local($user_ext); local($old_match_flag); # Enable multi-line matching $old_match_flag = $*; $* = 1; # For hypertext jumps, surround the text. If the # text contains a jump, the existing jump is removed. if ($attr{'jump'} ne '') { # Get the jump value. If an extension other than html is # requested, change the jump value accordingly. Also, # we make sure than any special characters are escaped. $value = $attr{'jump'}; $user_ext = $SDF_USER'var{'HTML_EXT'}; if ($user_ext) { $value =~ s/\.html/.$user_ext/; } $value = &_HtmlEscape($value); $text =~ s/\]+\>(.*)\<\/A\>/$1/; $text = "$text"; delete $attr{'jump'}; $result++; } # For hypertext ids, surround the text if it doesn't already contain # a jump. Otherwise, prefix the text with a dummy target so that # jump and id definitions don't clash. if ($attr{'id'} ne '') { $value = &_HtmlEscape($attr{'id'}); if ($text =~ /\ $text"; } else { $text = "$text"; } delete $attr{'id'}; $result++; } # Reset multi-line matching flag $* = $old_match_flag; # Return result return $result; } # # >>_Description:: # {{Y:_HtmlParaAppend}} merges {{para}} into the last paragraph # in {{@result}}. Both paragraphs are assumed to be PREformatted. # sub _HtmlParaAppend { local(*result, $para) = @_; # local(); substr($result[$#result], -6) = "$para\n"; } # # >>_Description:: # {{Y:_HtmlItemAppend}} merges a list item {{item}} into the current # output. The item before is assumed to be a list item too. # sub _HtmlItemAppend { local(*result, $item, $indent, $prev_indent, $para_tag, $prev_tag, *para_attrs) = @_; # local(); local($type, $prev_type); local($posn, $end_tokens); # Get the list type and previous type $type = substr($item, 1, 2); $prev_type = substr($result[$#result], -3, 2); # Indent is increasing if ($indent > $prev_indent) { $posn = -5 * $prev_indent; while (++$prev_indent < $indent) { $item = "<$type>$item"; } } # Indent is descreasing or the same else { # plain items are compatible with both ordered and unordered lists, so # we need to handle them separately if (substr($item, 4, 5) ne "\n
  • ") { $posn = -5 * $indent; $item = substr($item, 4, length($item) - 9); $item = "\n
    $item"; ## If the previous tag is the same but the indents differ, ## this is an enumerated list item so prepend another newline. #if ($para_tag eq $prev_tag && $prev_indent != $indent) { # $item = "\n
    $item"; #} } # handle items of an existing list elsif ($type eq $prev_type) { $posn = -5 * $indent; $item = substr($item, 4, length($item) - 9); } # item is not compatible with the current list - start a new one else { $indent--; $posn = $indent ? (-5 * $indent) : length($result[$#result]); } } # Merge the item $end_tokens = substr($result[$#result], $posn); substr($result[$#result], $posn) = "$item$end_tokens"; } # # >>_Description:: # {{Y:_HtmlHandlerTuning}} handles the 'tuning' directive. # sub _HtmlHandlerTuning { local(*outbuffer, $style, %attr) = @_; # local(); # do nothing } # # >>_Description:: # {{Y:_HtmlHandlerEndTuning}} handles the 'endtuning' directive. # sub _HtmlHandlerEndTuning { local(*outbuffer, $style, %attr) = @_; # local(); # do nothing } # # >>_Description:: # {{Y:_HtmlHandlerTable}} handles the 'table' directive. # sub _HtmlHandlerTable { local(*outbuffer, $columns, %attr) = @_; # local(); local($indent, $previous_indent, $posn, $begin_tokens, $end_tokens); local($header); local($tbl_title); # Handle tables inside a list # Note: the previous indent is available as a dynamically # scoped variable in &HtmlFormatSection $indent = $attr{'listitem'}; $begin_tokens = ''; $end_tokens = ''; if ($indent) { $previous_indent = $prev_indent; # get dynamically scoped var if ($indent > $previous_indent) { $posn = -5 * $previous_indent; while ($previous_indent++ < $indent) { $begin_tokens .= ""; } } else { $posn = -$indent * 5; } if ($posn < 0) { $end_tokens .= substr($outbuffer[$#outbuffer], $posn); substr($outbuffer[$#outbuffer], $posn) = $begin_tokens; } else { push(@outbuffer, $begin_tokens); } } # Update the state push(@_html_tbl_state, $_HTML_INTABLE); push(@_html_tbl_endtokens, $end_tokens); push(@_html_tbl_previndent, $indent); # Build the header $header = ' CLASS="' . &_HtmlEscape($attr{'style'}) . '"'; if ($attr{'style'} ne 'plain') { $header .= " BORDER"; } if (defined($attr{'cellspacing'})) { $header .= " CELLSPACING='$attr{'cellspacing'}'"; } if (defined($attr{'cellpadding'})) { $header .= " CELLPADDING='$attr{'cellpadding'}'"; } if ($attr{'align'}) { my $align = $attr{'align'}; $align = 'Left' if $align eq 'Inner'; $align = 'Right' if $align eq 'Outer'; $header .= " ALIGN='$align'"; } if ($attr{'bgcolor'}) { $header .= " BGCOLOR='$attr{'bgcolor'}'"; } # Update the output buffer push(@outbuffer, ""); # Add the title, if any $tbl_title = $attr{'title'}; if ($tbl_title ne '') { push(@outbuffer, "" . $tbl_title . ""); } } # # >>_Description:: # {{Y:_HtmlHandlerRow}} handles the 'row' directive. # sub _HtmlHandlerRow { local(*outbuffer, $text, %attr) = @_; # local(); local($state); # Finalise the old cell/row, if any $state = $_html_tbl_state[$#_html_tbl_state]; if ($state eq $_HTML_INCELL) { push(@outbuffer, "", ""); } elsif ($state eq $_HTML_INROW) { push(@outbuffer, ""); } # Update the state $_html_tbl_state[$#_html_tbl_state] = $_HTML_INROW; # Update the output buffer if ($text ne '' && $text ne 'Body') { push(@outbuffer, ""); } else { push(@outbuffer, ""); } } # # >>_Description:: # {{Y:_HtmlHandlerCell}} handles the 'cell' directive. # sub _HtmlHandlerCell { local(*outbuffer, $text, %attr) = @_; # local(); local($state); local($header); # Reset the paragraph counter for this cell $_html_cell_paracnt = (); # If the cell is hidden, output nothing return if $attr{'hidden'}; # Finalise the old cell, if any $state = $_html_tbl_state[$#_html_tbl_state]; if ($state eq $_HTML_INCELL) { push(@outbuffer, ""); } # Update the state $_html_tbl_state[$#_html_tbl_state] = $_HTML_INCELL; # Build the header $header = ''; if (defined($attr{'align'})) { $header .= " ALIGN='$attr{'align'}'"; } if (defined($attr{'valign'})) { $header .= " VALIGN='$attr{'valign'}'"; } if ($attr{'cols'} != 1) { $header .= " COLSPAN='$attr{'cols'}'"; } if ($attr{'rows'} != 1) { $header .= " ROWSPAN='$attr{'rows'}'"; } if (defined($attr{'nowrap'})) { $header .= " NOWRAP"; } if (defined($attr{'bgcolor'})) { $header .= " BGCOLOR='$attr{'bgcolor'}'"; } # Update the output buffer push(@outbuffer, ""); } # # >>_Description:: # {{Y:_HtmlHandlerEndTable}} handles the 'endtable' directive. # sub _HtmlHandlerEndTable { local(*outbuffer, $text, %attr) = @_; # local(); local($state); local($end_tokens); # Finalise the table $state = pop(@_html_tbl_state); if ($state eq $_HTML_INCELL) { push(@outbuffer, "", ""); } elsif ($state eq $_HTML_INROW) { push(@outbuffer, ""); } push(@outbuffer, ""); # Terminate the list, if any $end_tokens = pop(@_html_tbl_endtokens); push(@outbuffer, $end_tokens); # Restore the previous indent. We do this by hacking the # %para_attrs hash dynamically scoped in &HtmlFormatSection. :-( $para_attrs{'in'} = pop(@_html_tbl_previndent); } # # >>_Description:: # {{Y:_HtmlHandlerImport}} handles the import directive. # sub _HtmlHandlerImport { local(*outbuffer, $filepath, %attr) = @_; # local(); local($para); # Build the result &_HtmlPhraseHandlerImport(*para, $filepath, %attr); push(@outbuffer, &_HtmlElement('P', $para)); } # # >>_Description:: # {{Y:_HtmlHandlerInline}} handles the inline directive. # sub _HtmlHandlerInline { local(*outbuffer, $text, %attr) = @_; # local(); # Check we can handle this format my $target = $attr{'target'}; return unless $target eq 'html'; # Build the result push(@outbuffer, $text); } # # >>_Description:: # {{Y:_HtmlHandlerOutput}} handles the output directive. # sub _HtmlHandlerOutput { local(*outbuffer, $text, %attr) = @_; # local(); local($offset, @topic_data, @dummy_contents); local($file); local($this_topic); # Finalise the current topic, if requested if ($text eq '-') { # If there is no current topic, do nothing return unless @_html_topic_offset; # Generate the html for the topic $offset = pop(@_html_topic_offset); @topic_data = splice(@outbuffer, $offset + 1); #printf "TOPIC:\n%s\nENDTOPIC\n", join("\n", @topic_data); @topic_data = &_HtmlFinalise(*topic_data, *dummy_contents); # Output the topic $file = pop(@_html_topic_file); #print STDERR "offset: $offset, file: $file.\n"; unless (open(TOPIC, ">$file")) { &AppMsg("error", "unable to write to topic file '$file'"); return; } print TOPIC join("\n", @topic_data), "\n"; close(TOPIC); } # Otherwise, save the output filename and the current offset else { push(@_html_topic_file, $text); push(@_html_topic_offset, $#outbuffer); } # Update the current topic name (without the extension) $this_topic = $_html_topic_file[$#_html_topic_file]; $this_topic =~ s/\.html$//; $SDF_USER'var{'HTML_TOPIC'} = $this_topic; #print STDERR "HTML_TOPIC: $this_topic.\n"; } # # >>_Description:: # {{Y:_HtmlHandlerObject}} handles the 'object' directive. # sub _HtmlHandlerObject { local(*outbuffer, $text, %attrs) = @_; # local(); if ($text eq 'meta') { push(@_html_meta, "'); } if ($text eq 'link') { push(@_html_links, "'); } } # # >>_Description:: # {{Y:_HtmlHandlerStyleSheet}} handles the 'stylesheet' directive. # sub _HtmlHandlerStyleSheet { local(*outbuffer, $text, %attrs) = @_; # local(); push(@_html_stylesheet, $text); } # # >>_Description:: # {{Y:_HtmlHandlerDiv}} handles the 'div' directive. # sub _HtmlHandlerDiv { local(*outbuffer, $text, %attrs) = @_; # local(); push(@_html_divs, $text); push(@outbuffer, '
    '); } # # >>_Description:: # {{Y:_HtmlHandlerEndDiv}} handles the 'enddiv' directive. # sub _HtmlHandlerEndDiv { local(*outbuffer, $text, %attrs) = @_; # local(); push(@outbuffer, '
    '); # If we've just finished building the title, save it away # and re-initialise the buffer if (pop(@_html_divs) eq 'title') { @_html_title_div = @outbuffer; @outbuffer = (); } } # # >>_Description:: # {{Y:_HtmlPhraseHandlerChar}} handles the 'char' phrase directive. # sub _HtmlPhraseHandlerChar { local(*para, $text, %attr) = @_; # local(); # Map the symbolic names if (defined($_HTML_CHAR{$text})) { $para .= $_HTML_CHAR{$text}; } else { # Numbers are ISO character codes $para .= $text =~ /\D/ ? "&$text;" : "&#$text;"; } } # # >>_Description:: # {{Y:_HtmlPhraseHandlerImport}} handles the 'import' phrase directive. # sub _HtmlPhraseHandlerImport { local(*para, $filepath, %attr) = @_; # local(); local($name, $value); local($jump); local($pre, $post); if ( $attr{'align'} eq 'center' ) { $pre='
    '; $post='
    '; } else { $pre=''; $post=''; } # Map the attributes to HTML while (($name, $value) = each %attr) { # Simple for now delete $attr{$name} if $name eq 'fullname'; delete $attr{$name} if $name eq 'width'; delete $attr{$name} if $name eq 'height'; } # Build the result $para .= $pre; if ($attr{'jump'} ne '') { $jump = $attr{'jump'}; delete $attr{'jump'}; # Disable the border unless it is explicitly asked for $attr{'border'} = 0 unless $attr{'border'}; $para .= "" . ""; } else { $para .= ""; } $para .= $post; } # # >>_Description:: # {{Y:_HtmlPhraseHandlerInline}} handles the 'inline' phrase directive. # sub _HtmlPhraseHandlerInline { local(*para, $text, %attr) = @_; # local(); # Build the result $para .= $text; } # # >>_Description:: # {{Y:_HtmlPhraseHandlerVariable}} handles the 'variable' phrase directive. # sub _HtmlPhraseHandlerVariable { local(*para, $text, %attr) = @_; # local(); # do nothing } package SDF_USER; # # >>Description:: # {{Y:HtmlTopicsModeHeading}} is an event processing routine for # headings when topics mode is enabled. # sub HtmlTopicsModeHeading { # local() = @_; # local(); local($level, $file_base); local($jump); local($topic_base, $topic_file); local(@prepend); local($new_level, $close_count, $i); local($title); # As the heading for the main document might be built in the # "front" component, we explicitly ignore headings in sdm files return if $var{'FILE_EXT'} eq 'sdm'; # If this heading doesn't have an id, ignore it return if $attr{'noid'}; # Get the heading level and containing file $level = substr($style, 1, 1); $file_base = $var{'FILE_BASE'}; # When processing the main file: # * detect the first heading in each SDF file or a # certain level heading as a topic boundary (and save # the heading text as the label for that topic) # * save the file each section lives in, so that # section jumps work as expected if ($var{'HTML_TOPICS_MODE'}) { if (! $topic_label{$file_base} && $file_base ne $var{'DOC_BASE'}) { $'_html_topic = $file_base; push(@levels, $level); push(@topics, $'_html_topic); $topic_label{$'_html_topic} = $text; $topic_level{$'_html_topic} = $level; $jump = $'_html_topic . ".html"; $'_html_topic_start{$file_base,$text} = $'_html_topic; } elsif ($level <= $var{'OPT_SPLIT_LEVEL'}) { if ($'_html_topic_start{$file_base,$text}) { &'AppMsg("warning", "file base '$file_base' & topic heading '$text' combination is not unique'"); return; } $'_html_topic = &HtmlTopicName($var{'DOC_BASE'}); push(@levels, $level); push(@topics, $'_html_topic); $topic_label{$'_html_topic} = $text; $topic_level{$'_html_topic} = $level; $jump = $'_html_topic . ".html"; $'_html_topic_start{$file_base,$text} = $'_html_topic; } else { if ($attr{'id'} ne '') { $jump = $'_html_topic . ".html#" . $attr{"id"}; } else { $jump = $'_html_topic . ".html#" . &TextToId($text); } } # Save the jump for this file/text combination. # This is used for TOC generation. $'_html_jump_id{$file_base,$text} = $jump; # Save the place to jump to for this text. # The jump table is used to resolve SECT jumps (in topics mode). if ($jump{$text} eq '') { $jump{$text} = $jump; $jump_level{$text} = $level; } else { # Override the jump if the new jump is more important if ($level < $jump_level{$text}) { $jump{$text} = $jump; $jump_level{$text} = $level; } } } # Otherwise, we're creating sub-topics else { # If this heading starts a topic: # * prepend the necessary output directives # * make it the title # * prevent a line above it by setting the notoc attribute. $topic_base = $'_html_topic_start{$file_base,$text}; if ($topic_base) { $topic_file = "$topic_base.html"; @prepend = (); $new_level = $topic_level{$topic_base}; $close_count = $'_html_topic_level - $new_level + 1; $'_html_topic_level = $new_level; for ($i = 0; $i < $close_count; $i++) { push(@prepend, "!output '-'"); } $title = $text; $title =~ s/(['\\])/\\$1/g; push(@prepend, "[jump='$topic_file'] $text", "!output '$topic_file'", #"!define HTML_TOPIC '$topic_base'", "!define DOC_TITLE '$title'", "!HTML_BUILD_TITLE"); &PrependText(@prepend); $attr{'notoc'} = 1; } } } # Generate a name for a topic sub HtmlTopicName { local($base) = @_; local($tname); $'_html_topic_cntr++; $tname = $var{'HTML_TOPIC_PATTERN'}; $tname = '$b_$n' if $tname eq ''; $tname =~ s/\$b/$base/g; $tname =~ s/\$n/$'_html_topic_cntr/; return $tname; } # package return value 1;