| # HTMLNonXS.pm: output tree as HTML. Implementation of functions with |
| # an XS interface |
| # |
| # Copyright 2011-2026 Free Software Foundation, Inc. |
| # |
| # 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 3 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, see <https://www.gnu.org/licenses/>. |
| # |
| # Original author: Patrice Dumas <pertusus@free.fr> |
| |
| # ALTIMP XSTexinfo/convert/ConvertHTMLXS.xs |
| # ALTIMP C/convert/*html*.[ch] |
| |
| # General information on the HTML converter is in HTML.pm comments |
| |
| package Texinfo::Convert::HTML; |
| |
| # charnames::vianame is not documented in 5.6.0. |
| use 5.008; |
| |
| # See 'The "Unicode Bug"' under 'perlunicode' man page. This means |
| # that regular expressions will treat characters 128-255 in a Perl string |
| # the same regardless of whether the string is using a UTF-8 encoding. |
| # For older Perls, you can use utf8::upgrade on the strings, where the |
| # difference matters. |
| # Also follows unicode rules for uc() and lc (). |
| use if $] >= 5.012, feature => 'unicode_strings'; |
| |
| use if $] >= 5.014, re => '/a'; # ASCII-only character classes in regexes |
| |
| use strict; |
| use warnings; |
| |
| # To check if there is no erroneous autovivification |
| #no autovivification qw(fetch delete exists store strict); |
| |
| use Carp qw(cluck confess); |
| |
| use File::Copy qw(copy); |
| |
| use File::Spec; |
| |
| my $updir = File::Spec->updir(); |
| |
| use Storable; |
| |
| use Encode qw(find_encoding decode); |
| use charnames (); |
| |
| use Texinfo::Commands; |
| use Texinfo::Options; |
| use Texinfo::HTMLData; |
| |
| use Texinfo::TreeElement; |
| |
| use Texinfo::Common; |
| use Texinfo::Indices; |
| |
| use Texinfo::Convert::Unicode; |
| use Texinfo::Convert::Texinfo; |
| use Texinfo::Convert::Utils; |
| use Texinfo::Convert::Text; |
| use Texinfo::Convert::NodeNameNormalization; |
| use Texinfo::ManipulateTree; |
| use Texinfo::Structuring; |
| use Texinfo::OutputUnits; |
| |
| # copy_options_for_convert_to_latex_math |
| use Texinfo::Convert::LaTeX; |
| |
| my %sectioning_heading_commands = %Texinfo::Commands::sectioning_heading_commands; |
| my %brace_commands = %Texinfo::Commands::brace_commands; |
| my %block_commands = %Texinfo::Commands::block_commands; |
| my %root_commands = %Texinfo::Commands::root_commands; |
| my %preformatted_commands = %Texinfo::Commands::preformatted_commands; |
| my %math_commands = %Texinfo::Commands::math_commands; |
| my %preformatted_code_commands = %Texinfo::Commands::preformatted_code_commands; |
| |
| my %brace_code_commands = %Texinfo::Commands::brace_code_commands; |
| my %small_block_associated_command = %Texinfo::Common::small_block_associated_command; |
| |
| our %HTML_align_commands; |
| |
| my %composition_context_commands = (%preformatted_commands, %root_commands, |
| %HTML_align_commands); |
| $composition_context_commands{'float'} = 1; |
| my %format_context_commands = (%block_commands, %root_commands); |
| # also defined in HTML.pm |
| my %format_raw_commands; |
| foreach my $block_command (keys(%block_commands)) { |
| $composition_context_commands{$block_command} = 1 |
| if ($block_commands{$block_command} eq 'menu'); |
| if ($block_commands{$block_command} eq 'format_raw') { |
| $format_raw_commands{$block_command} = 1; |
| delete $format_context_commands{$block_command}; |
| } |
| } |
| |
| foreach my $misc_context_command('tab', 'item', 'itemx', 'headitem') { |
| $format_context_commands{$misc_context_command} = 1; |
| } |
| |
| |
| |
| my $direction_orders = Texinfo::HTMLData::get_directions_order(); |
| # 'global', 'text', 'relative', 'file' |
| my @global_directions_order = @{$direction_orders->[0]}; |
| my @text_directions_order = @{$direction_orders->[1]}; |
| |
| # API for html formatting |
| |
| # ALTIMP C/convert/html_prepare_converter.c html_set_global_direction |
| # similar to texinfo_register_global_direction in Texinfo::Config, to be |
| # used to modify global directions after the converter initialization, |
| # but before association of global directions with output units |
| sub set_global_direction($$;$) { |
| my ($self, $direction, $node_texi_name) = @_; |
| |
| if (!$self->{'all_directions'}->{$direction}) { |
| $self->converter_document_warn( |
| sprintf(__("not setting an unknown direction: %s"), $direction)); |
| return; |
| } |
| if (!exists($self->{'customized_global_directions'}) |
| or !exists($self->{'customized_global_directions'}->{$direction})) { |
| if (not grep {$_ eq $direction} @global_directions_order) { |
| if (defined($node_texi_name)) { |
| $self->converter_document_warn(sprintf(__( |
| "keep direction %s, do not set to `%s'"), $direction, |
| $node_texi_name)); |
| } else { |
| $self->converter_document_warn(sprintf(__( |
| "keep direction %s"), $direction)); |
| } |
| return; |
| } |
| } |
| $self->{'customized_global_directions'} = {} |
| if (!exists($self->{'customized_global_directions'})); |
| $self->{'customized_global_directions'}->{$direction} = $node_texi_name; |
| return; |
| } |
| |
| sub _collect_css_element_class($$) { |
| my ($self, $element_class) = @_; |
| |
| #if (not $self->{'document_global_context_counter'} |
| # and not exists($self->{'current_filename'})) { |
| # cluck "BUG: $element_class: CSS no current file"; |
| #} |
| |
| if (exists($self->{'css_element_class_styles'}->{$element_class})) { |
| if ($self->{'document_global_context_counter'}) { |
| $self->{'document_global_context_css'}->{$element_class} = 1; |
| } elsif (exists($self->{'current_filename'})) { |
| $self->{'page_css'}->{$self->{'current_filename'}} = {} |
| if (!exists($self->{'page_css'}->{$self->{'current_filename'}})); |
| $self->{'page_css'}->{$self->{'current_filename'}}->{$element_class} = 1; |
| } |
| } |
| } |
| |
| #my $characters_replaced_from_class_names = quotemeta('[](),~#:/\\@+=!;.,?* '); |
| # Not clear what character should be allowed and which ones replaced |
| # besides space. Not really important as the caller should themselves |
| # sanitize the class names already. |
| my $characters_replaced_from_class_names = quotemeta(' '); |
| sub _protect_class_name($$) { |
| my ($self, $class_name) = @_; |
| |
| $class_name =~ s/[$characters_replaced_from_class_names]/-/g; |
| |
| # API info: using the API to allow for customization would be: |
| # return &{$self->formatting_function('format_protect_text')}($self, $class_name); |
| return _default_format_protect_text($self, $class_name); |
| } |
| |
| # $classes should be an array reference or undef |
| sub html_attribute_class($$;$) { |
| my ($self, $element, $classes) = @_; |
| |
| if (defined($classes) and ref($classes) ne 'ARRAY') { |
| confess("html_attribute_class: $classes not an array ref (for $element)"); |
| } |
| if (!defined($classes) or scalar(@$classes) == 0 |
| or $self->get_conf('NO_CSS')) { |
| if ($element eq 'span') { |
| return ''; |
| } else { |
| return "<$element"; |
| } |
| } |
| |
| my $style = ''; |
| |
| if ($self->get_conf('INLINE_CSS_STYLE')) { |
| my @styles = (); |
| foreach my $style_class (@$classes) { |
| if (not defined($style_class)) { |
| confess("class not defined (for $element)"); |
| } |
| if (exists($self->{'css_element_class_styles'} |
| ->{"$element.$style_class"})) { |
| push @styles, |
| $self->{'css_element_class_styles'}->{"$element.$style_class"}; |
| } |
| } |
| if (scalar(@styles) > 0) { |
| $style = ' style="'.join(';', @styles).'"'; |
| } |
| } else { |
| foreach my $style_class (@$classes) { |
| if (not defined($style_class)) { |
| confess ("class not defined (for $element)"); |
| } |
| _collect_css_element_class($self, "$element.$style_class"); |
| } |
| } |
| my $class_str = join(' ', map {_protect_class_name($self, $_)} @$classes); |
| return "<$element class=\"$class_str\"$style"; |
| } |
| |
| # CSS |
| |
| # returns an array of CSS element.class seen in the $FILENAME |
| sub html_get_css_elements_classes($;$) { |
| my ($self, $filename) = @_; |
| |
| my %css_elements_classes; |
| if (exists($self->{'document_global_context_css'})) { |
| %css_elements_classes = ( %{$self->{'document_global_context_css'}} ); |
| } |
| |
| if (defined($filename) and exists($self->{'page_css'}) |
| and exists($self->{'page_css'}->{$filename})) { |
| %css_elements_classes = ( %css_elements_classes, |
| %{$self->{'page_css'}->{$filename}} ); |
| } |
| |
| if ($css_elements_classes{'a.copiable-link'}) { |
| $css_elements_classes{'span:hover a.copiable-link'} = 1; |
| } |
| |
| my @result = sort(keys(%css_elements_classes)); |
| return \@result; |
| } |
| |
| sub css_add_info($$$) { |
| my ($self, $spec, $css_info) = @_; |
| |
| if ($spec eq 'rules') { |
| push @{$self->{'css_rule_lines'}}, $css_info; |
| } elsif ($spec eq 'imports') { |
| push @{$self->{'css_import_lines'}}, $css_info; |
| } |
| } |
| |
| sub css_set_selector_style($$$) { |
| my ($self, $css_info, $css_style) = @_; |
| |
| if (!defined($css_style)) { |
| delete($self->{'css_element_class_styles'}->{$css_info}); |
| } else { |
| $self->{'css_element_class_styles'}->{$css_info} = $css_style; |
| } |
| } |
| |
| sub css_get_info($$) { |
| my ($self, $spec) = @_; |
| |
| my @empty_array; |
| |
| if ($spec eq 'rules') { |
| if (defined($self->{'css_rule_lines'})) { |
| return $self->{'css_rule_lines'}; |
| } else { |
| return \@empty_array; |
| } |
| } elsif ($spec eq 'imports') { |
| if (defined($self->{'css_import_lines'})) { |
| return $self->{'css_import_lines'}; |
| } else { |
| return \@empty_array; |
| } |
| } else { |
| my @result = sort(keys(%{$self->{'css_element_class_styles'}})); |
| return \@result; |
| } |
| } |
| |
| sub css_get_selector_style($$) { |
| my ($self, $css_info) = @_; |
| |
| if (exists($self->{'css_element_class_styles'}->{$css_info})) { |
| return $self->{'css_element_class_styles'}->{$css_info}; |
| } else { |
| return undef; |
| } |
| } |
| |
| # flags used to specify the conversion contexts. |
| our $CTXF_string; |
| our $CTXF_code; |
| our $CTXF_normal; |
| |
| our %special_list_mark_css_string_no_arg_command; |
| |
| |
| |
| # API to access converter state for conversion |
| |
| sub in_math($) { |
| my $self = shift; |
| |
| return $self->{'document_context'}->[-1]->{'math'}; |
| } |
| |
| # set if in menu or preformatted command |
| sub in_preformatted_context($) { |
| my $self = shift; |
| |
| return $self->{'document_context'}->[-1]->{'preformatted_context'}->[-1]; |
| } |
| |
| sub inside_preformatted($) { |
| my $self = shift; |
| |
| return $self->{'document_context'}->[-1]->{'inside_preformatted'}; |
| } |
| |
| sub in_upper_case($) { |
| my $self = shift; |
| |
| return $self->{'document_context'}->[-1]->{'formatting_context'}->[-1] |
| ->{'upper_case'}; |
| } |
| |
| sub in_non_breakable_space($) { |
| my $self = shift; |
| |
| return $self->{'document_context'}->[-1]->{'formatting_context'}->[-1] |
| ->{'no_break'}; |
| } |
| |
| sub in_space_protected($) { |
| my $self = shift; |
| |
| return $self->{'document_context'}->[-1]->{'formatting_context'}->[-1] |
| ->{'space_protected'}; |
| } |
| |
| sub in_code($) { |
| my $self = shift; |
| |
| return $self->{'document_context'}->[-1]->{'monospace'}->[-1]; |
| } |
| |
| sub in_string($) { |
| my $self = shift; |
| |
| return $self->{'document_context'}->[-1]->{'string'}; |
| } |
| |
| sub in_verbatim($) { |
| my $self = shift; |
| |
| return $self->{'document_context'}->[-1]->{'verbatim'}; |
| } |
| |
| sub in_raw($) { |
| my $self = shift; |
| |
| return $self->{'document_context'}->[-1]->{'raw'}; |
| } |
| |
| sub in_multiple_conversions($) { |
| my $self = shift; |
| |
| return scalar(@{$self->{'multiple_pass'}}); |
| } |
| |
| sub paragraph_number($) { |
| my $self = shift; |
| |
| return $self->{'document_context'}->[-1]->{'formatting_context'}->[-1] |
| ->{'paragraph_number'}; |
| } |
| |
| sub preformatted_number($) { |
| my $self = shift; |
| |
| return $self->{'document_context'}->[-1]->{'formatting_context'}->[-1] |
| ->{'preformatted_number'}; |
| } |
| |
| sub top_block_command($) { |
| my $self = shift; |
| |
| return $self->{'document_context'}->[-1]->{'block_commands'}->[-1]; |
| } |
| |
| sub preformatted_classes_stack($) { |
| my $self = shift; |
| |
| return $self->{'document_context'}->[-1]->{'preformatted_classes'}; |
| } |
| |
| sub in_align($) { |
| my $self = shift; |
| |
| my $context |
| = $self->{'document_context'}->[-1]->{'composition_context'}->[-1]; |
| if (exists($HTML_align_commands{$context})) { |
| return $context; |
| } else { |
| return undef; |
| } |
| } |
| |
| sub multi_expanded_region($) { |
| my $self = shift; |
| |
| if (scalar(@{$self->{'multiple_pass'}})) { |
| return $self->{'multiple_pass'}->[-1]; |
| } |
| return undef; |
| } |
| |
| sub count_elements_in_filename($$$) { |
| my ($self, $spec, $filename) = @_; |
| |
| if (!defined($filename)) { |
| confess("count_elements_in_filename: filename undef"); |
| } |
| |
| if ($spec eq 'total') { |
| if (defined($self->{'elements_in_file_count'}->{$filename})) { |
| return $self->{'elements_in_file_count'}->{$filename}; |
| } |
| } elsif ($spec eq 'remaining') { |
| if (defined($self->{'file_counters'}->{$filename})) { |
| return $self->{'file_counters'}->{$filename}; |
| } |
| } elsif ($spec eq 'current') { |
| if (defined($self->{'file_counters'}->{$filename})) { |
| return $self->{'elements_in_file_count'}->{$filename} |
| - $self->{'file_counters'}->{$filename} +1; |
| } |
| } |
| return undef; |
| } |
| |
| sub is_format_expanded($$) { |
| my ($self, $format) = @_; |
| |
| return $self->{'expanded_formats'}->{$format}; |
| } |
| |
| sub _set_code_context($$) { |
| my ($self, $code) = @_; |
| |
| push @{$self->{'document_context'}->[-1]->{'monospace'}}, $code; |
| } |
| |
| sub _pop_code_context($) { |
| my $self = shift; |
| |
| pop @{$self->{'document_context'}->[-1]->{'monospace'}}; |
| } |
| |
| |
| |
| # API for links and elements directions formatting. |
| |
| # the main data structure of the element target API is a hash reference, called |
| # the target information. |
| # The 'target' and 'filename' keys should be set for every type of element, |
| # but the other keys will only be set on some elements. |
| # |
| # The following keys can be set: |
| # |
| # Strings |
| # |
| # 'target': A unique string representing the target. Used as argument to |
| # 'id' attribute. |
| # 'contents_target': A unique string representing the target to the location |
| # of the element in the table of content. |
| # 'shortcontents_target': A unique string representing the target to the |
| # location of the element in the short table of contents |
| # 'node_filename': the file name deriving from the element node name |
| # 'section_filename': the file name deriving from the element section name |
| # 'special_unit_filename': the file name of special elements |
| # (separate contents, about...) |
| # 'filename': the file name the element content is output to |
| # 'text', 'text_nonumber': a textual representation of the element where |
| # there is no restriction on the text formatting (ie HTML elements |
| # can be used). |
| # With _nonumber, no section number. |
| # 'string', 'string_nonumber': a textual representation of the element with |
| # restrictions on the available formatting, in practice no |
| # HTML elements, only entities to be able to use in attributes. |
| # With _nonumber, no section number. |
| # |
| # Other types |
| # |
| # 'tree', 'tree_nonumber: a Texinfo tree element which conversion should |
| # correspond to the element name. |
| # With _nonumber, no section number. |
| # 'node_command': the node element associated with the target element. |
| # 'root_element_command': the command associated to the top level element |
| # associated with the target element. |
| # |
| # Some functions cache their results in these hashes. |
| |
| # $COMMAND should be a tree element which is a possible target of a link. |
| # return the target information. |
| sub _get_target($$) { |
| my ($self, $command) = @_; |
| |
| if (!defined($command)) { |
| cluck("_get_target command argument not defined"); |
| } |
| |
| if (exists($self->{'targets'}->{$command})) { |
| return $self->{'targets'}->{$command}; |
| } |
| |
| return undef; |
| } |
| |
| # API for links and elements directions formatting |
| |
| # This returns the id specific of the $COMMAND tree element |
| sub command_id($$) { |
| my ($self, $command) = @_; |
| |
| my $target = _get_target($self, $command); |
| if (defined($target)) { |
| return $target->{'target'}; |
| } else { |
| return undef; |
| } |
| } |
| |
| sub command_contents_target($$$) { |
| my ($self, $command, $contents_or_shortcontents) = @_; |
| |
| $contents_or_shortcontents = 'shortcontents' |
| if ($contents_or_shortcontents eq 'summarycontents'); |
| |
| my $target = _get_target($self, $command); |
| if (defined($target)) { |
| return $target->{$contents_or_shortcontents .'_target'}; |
| } else { |
| return undef; |
| } |
| } |
| |
| sub _get_footnote_location_target($$) { |
| my ($self, $command) = @_; |
| |
| if (exists($self->{'special_targets'}) |
| and exists($self->{'special_targets'}->{'footnote_location'}) |
| and exists($self->{'special_targets'}->{'footnote_location'}->{$command})) { |
| return $self->{'special_targets'}->{'footnote_location'}->{$command}; |
| } |
| return undef; |
| } |
| |
| sub footnote_location_target($$) { |
| my ($self, $command) = @_; |
| |
| my $footnote_location_special_target_info |
| = _get_footnote_location_target($self, $command); |
| if (defined($footnote_location_special_target_info)) { |
| return $footnote_location_special_target_info->{'target'}; |
| } |
| return undef; |
| } |
| |
| sub command_filename($$) { |
| my ($self, $command) = @_; |
| |
| my $target = _get_target($self, $command); |
| if (defined($target)) { |
| if (exists($target->{'filename'})) { |
| return $target->{'filename'}; |
| } |
| # this finds a special element for footnote command if such an element |
| # exists. This is best, the special element filename is the footnote |
| # filename. |
| my ($root_element, $root_command) |
| = _html_get_tree_root_element($self, $command, 1); |
| |
| if (defined($root_element) |
| and exists($root_element->{'unit_filename'})) { |
| $target->{'filename'} |
| = $root_element->{'unit_filename'}; |
| return $root_element->{'unit_filename'}; |
| } else { |
| $target->{'filename'} = undef; |
| } |
| } |
| return undef; |
| } |
| |
| sub command_root_element_command($$) { |
| my ($self, $command) = @_; |
| |
| my $target = _get_target($self, $command); |
| if (defined($target)) { |
| if (not exists($target->{'root_element_command'})) { |
| # in contrast with command_filename() we find the root element through |
| # the location holding the @footnote command. It is better, as the |
| # footnote special element is not associated with a root command, |
| # it is better to stay in the document to find a root element. |
| my ($root_element, $root_command) |
| = _html_get_tree_root_element($self, $command); |
| if (defined($root_element) and $root_element->{'unit_type'} eq 'unit') { |
| $target->{'root_element_command'} |
| = $root_element->{'unit_command'}; |
| } else { |
| $target->{'root_element_command'} = undef; |
| } |
| } |
| return $target->{'root_element_command'}; |
| } |
| return undef; |
| } |
| |
| sub command_node($$) { |
| my ($self, $command) = @_; |
| |
| my $target = _get_target($self, $command); |
| if (defined($target)) { |
| if (not exists($target->{'node_command'})) { |
| # this finds a special element for footnote command if |
| # such an element exists |
| my ($root_element, $root_command) |
| = _html_get_tree_root_element($self, $command, 1); |
| if (defined($root_command) and exists($root_command->{'cmdname'})) { |
| if ($root_command->{'cmdname'} eq 'node') { |
| $target->{'node_command'} = $root_command; |
| } elsif (exists($self->{'document'})) { |
| my $sections_list = $self->{'document'}->sections_list(); |
| my $section_relations |
| = $sections_list->[$root_command->{'extra'}->{'section_number'} -1]; |
| if (exists($section_relations->{'associated_node'})) { |
| $target->{'node_command'} |
| = $section_relations->{'associated_node'}->{'element'}; |
| } |
| } |
| } else { |
| $target->{'node_command'} = undef; |
| } |
| } |
| return $target->{'node_command'}; |
| } |
| return undef; |
| } |
| |
| # $SPECIFIED_TARGET can be used to specify explicitly the target |
| sub _internal_command_href($$;$$) { |
| my ($self, $command, $source_filename, $specified_target) = @_; |
| |
| $source_filename = $self->{'current_filename'} |
| if (!defined($source_filename)); |
| |
| my $target; |
| if (defined($specified_target)) { |
| $target = $specified_target; |
| } else { |
| my $target_command = $command; |
| # for sectioning command prefer the associated node. If there is no |
| # associated node, use the associated_anchor_command. This order |
| # is important for sectioning commands, it means that in the following |
| # case the @chapter href will be given by the @node, even if there is |
| # an @xrefname in-between. |
| # |
| # @node my node |
| # @xrefname name for my node |
| # |
| # @chapter Chapter without directly associated node |
| if (exists($self->{'document'}) and exists($command->{'extra'})) { |
| if ($command->{'extra'}->{'section_number'}) { |
| my $sections_list = $self->{'document'}->sections_list(); |
| my $section_relations |
| = $sections_list->[$command->{'extra'}->{'section_number'} -1]; |
| |
| if (exists($section_relations->{'associated_node'})) { |
| $target_command |
| = $section_relations->{'associated_node'}->{'element'}; |
| } elsif (exists($section_relations->{'associated_anchor_command'})) { |
| $target_command |
| = $section_relations->{'associated_anchor_command'}->{'element'}; |
| } |
| } elsif ($command->{'extra'}->{'heading_number'}) { |
| my $headings_list = $self->{'document'}->headings_list(); |
| my $heading_relations |
| = $headings_list->[$command->{'extra'}->{'heading_number'} -1]; |
| |
| if (exists($heading_relations->{'associated_anchor_command'})) { |
| $target_command |
| = $heading_relations->{'associated_anchor_command'}->{'element'}; |
| } |
| } |
| } |
| |
| my $target_information = _get_target($self, $target_command); |
| $target = $target_information->{'target'} if (defined($target_information)); |
| } |
| return undef if (!defined($target)); |
| my $href = ''; |
| |
| my $target_filename = $self->command_filename($command); |
| if (!defined($target_filename)) { |
| # Happens if there are no pages, for example if OUTPUT is set to '' |
| # as in the test cases. Also for things in @titlepage when |
| # titlepage is not output. |
| if (exists($self->{'document_units'}->[0]->{'unit_filename'})) { |
| # In that case use the first page. |
| $target_filename |
| = $self->{'document_units'}->[0]->{'unit_filename'}; |
| } |
| } |
| if (defined($target_filename)) { |
| if (!defined($source_filename) |
| or $source_filename ne $target_filename) { |
| $href .= $self->url_protect_file_text($target_filename); |
| # omit target if the command is an element command, there is only |
| # one element in file and there is a file in the href |
| my $command_root_element_command |
| = $self->command_root_element_command($command); |
| if (defined($source_filename) |
| and defined($command_root_element_command)) { |
| my $possible_empty_target = 0; |
| if ($command_root_element_command eq $command) { |
| $possible_empty_target = 1; |
| } elsif (exists($command_root_element_command->{'cmdname'}) |
| and $command_root_element_command->{'cmdname'} eq 'node' |
| and exists($command_root_element_command->{'extra'}) |
| and $command_root_element_command->{'extra'}->{'node_number'} |
| and exists($self->{'document'})) { |
| my $nodes_list = $self->{'document'}->nodes_list(); |
| my $node_relations |
| = $nodes_list->[$command_root_element_command |
| ->{'extra'}->{'node_number'} -1]; |
| if (exists($node_relations->{'associated_section'}) |
| and $node_relations->{'associated_section'}->{'element'} eq $command) { |
| $possible_empty_target = 1; |
| } |
| } |
| if ($possible_empty_target) { |
| my $count_elements_in_file |
| = $self->count_elements_in_filename('total', $target_filename); |
| if (defined($count_elements_in_file) and $count_elements_in_file == 1) { |
| $target = ''; |
| } |
| } |
| } |
| } |
| } |
| $href .= '#' . $target if ($target ne ''); |
| |
| if ($href eq '') { |
| return undef; |
| } |
| return $href; |
| } |
| |
| our %contents_command_special_unit_variety; |
| |
| # Return string for linking to $CONTENTS_OR_SHORTCONTENTS associated |
| # element from $COMMAND with <a href> |
| sub command_contents_href($$$;$) { |
| my ($self, $command, $contents_or_shortcontents, $source_filename) = @_; |
| |
| $source_filename = $self->{'current_filename'} |
| if (not defined($source_filename)); |
| |
| my ($special_unit_variety, $special_unit, $class_base, |
| $special_unit_direction) |
| = $self->command_name_special_unit_information($contents_or_shortcontents); |
| my $target |
| = $self->command_contents_target($command, $contents_or_shortcontents); |
| my $target_filename; |
| # !defined happens when called as convert() and not output() |
| if (defined($special_unit)) { |
| my $command = $special_unit->{'unit_command'}; |
| $target_filename = $self->command_filename($command); |
| } |
| my $href = ''; |
| if (defined($target_filename) and |
| (!defined($source_filename) |
| or $source_filename ne $target_filename)) { |
| $href .= $target_filename; |
| } |
| $href .= '#' . $target if ($target ne ''); |
| |
| if ($href eq '') { |
| return undef; |
| } |
| |
| return $href; |
| } |
| |
| sub footnote_location_href($$;$$$) { |
| my ($self, $command, $source_filename, $specified_target, |
| $target_filename) = @_; |
| |
| $source_filename = $self->{'current_filename'} |
| if (not defined($source_filename)); |
| |
| my $footnote_location_target_info |
| = _get_footnote_location_target($self, $command); |
| my $target = ''; |
| if (defined($specified_target)) { |
| $target = $specified_target; |
| } elsif (defined($footnote_location_target_info)) { |
| $target = $footnote_location_target_info->{'target'}; |
| } |
| # In the default footnote formatting functions, which calls |
| # footnote_location_href, the target file is always known as the |
| # footnote in the document appears before the footnote text formatting. |
| # $target_filename is therefore always defined. It is a good thing |
| # for the case of @footnote being formatted more than once (in multiple |
| # @insertcopying for instance) as the file found just below may not be the |
| # correct one in such a case. |
| if (not defined($target_filename)) { |
| if (defined($footnote_location_target_info) |
| and defined($footnote_location_target_info->{'filename'})) { |
| $target_filename = $footnote_location_target_info->{'filename'}; |
| } else { |
| # in contrast with command_filename() we find the location holding |
| # the @footnote command, not the footnote element with footnotes |
| my ($root_element, $root_command) |
| = _html_get_tree_root_element($self, $command); |
| if (defined($root_element)) { |
| if (not defined($footnote_location_target_info)) { |
| $self->{'special_targets'}->{'footnote_location'}->{$command} = {}; |
| $footnote_location_target_info |
| = $self->{'special_targets'}->{'footnote_location'}->{$command}; |
| } |
| $footnote_location_target_info->{'filename'} |
| = $root_element->{'unit_filename'}; |
| $target_filename = $footnote_location_target_info->{'filename'}; |
| } |
| } |
| } |
| my $href = ''; |
| if (defined($target_filename) and |
| (!defined($source_filename) |
| or $source_filename ne $target_filename)) { |
| $href .= $target_filename; |
| } |
| $href .= '#' . $target if ($target ne ''); |
| return $href; |
| } |
| |
| # Currently the only possibility for $TYPE is heading |
| sub _special_unit_info_tree($$$) { |
| my ($self, $type, $special_unit_variety) = @_; |
| |
| if (exists($self->{'translated_special_unit_info_texinfo'}->{$type})) { |
| if (not exists($self->{'translated_special_unit_info_tree'}->{$type} |
| ->{$special_unit_variety})) { |
| my $special_unit_info_texinfo_string |
| = $self->{'translated_special_unit_info_texinfo'}->{$type} |
| ->{$special_unit_variety}; |
| my $translated_tree; |
| if (defined($special_unit_info_texinfo_string)) { |
| # NOTE to be kept in sync with generated context in |
| # generate_code_convert_data.pl |
| my $translation_context = "$special_unit_variety section $type"; |
| $translated_tree = $self->pcdt($translation_context, |
| $special_unit_info_texinfo_string); |
| } |
| $self->{'translated_special_unit_info_tree'}->{$type} |
| ->{$special_unit_variety} |
| = $translated_tree; |
| return $translated_tree; |
| } else { |
| return $self->{'translated_special_unit_info_tree'}->{$type} |
| ->{$special_unit_variety}; |
| } |
| } |
| return undef; |
| } |
| |
| sub _internal_command_tree($$$) { |
| my ($self, $command, $no_number) = @_; |
| |
| my $target = _get_target($self, $command); |
| if (defined($target)) { |
| if (!exists($target->{'tree'})) { |
| my $tree; |
| my $in_code; |
| if (exists($command->{'type'}) |
| and $command->{'type'} eq 'special_unit_element') { |
| my $special_unit_variety |
| = $command->{'associated_unit'}->{'special_unit_variety'}; |
| $tree |
| = $self->_special_unit_info_tree('heading', |
| $special_unit_variety); |
| } elsif (exists($command->{'cmdname'}) |
| and ($command->{'cmdname'} eq 'node' |
| or $command->{'cmdname'} eq 'anchor' |
| or $command->{'cmdname'} eq 'namedanchor')) { |
| # to be a target, the node or anchor cannot be empty (nor expand to |
| # spaces only), so argument is necessarily set. |
| my $label_element; |
| if ($command->{'cmdname'} eq 'anchor' |
| or $command->{'cmdname'} eq 'namedanchor') { |
| $label_element = $command->{'contents'}->[0]; |
| } else { |
| # arguments_line type element |
| my $arguments_line = $command->{'contents'}->[0]; |
| $label_element = $arguments_line->{'contents'}->[0]; |
| } |
| $tree = $label_element; |
| $in_code = 1; |
| } elsif (exists($command->{'cmdname'}) |
| and ($command->{'cmdname'} eq 'float')) { |
| $tree = $self->float_type_number($command); |
| } else { |
| my $line_arg; |
| if (exists($root_commands{$command->{'cmdname'}})) { |
| # arguments_line type element |
| my $arguments_line = $command->{'contents'}->[0]; |
| $line_arg = $arguments_line->{'contents'}->[0]; |
| } else { |
| # @heading* commands |
| $line_arg = $command->{'contents'}->[0]; |
| } |
| if (!Texinfo::Common::empty_spaces_argument($line_arg)) { |
| my $section_number; |
| $section_number = $command->{'extra'}->{'section_heading_number'} |
| if (exists($command->{'extra'}) |
| and defined($command->{'extra'}->{'section_heading_number'})); |
| if ($section_number |
| and ($self->get_conf('NUMBER_SECTIONS') |
| or !defined($self->get_conf('NUMBER_SECTIONS')))) { |
| # NOTE since there is a copy, the elements that are found using their |
| # hash won't be found during conversion. This is the case for |
| # target elements such as @anchor. |
| my $substituted_strings |
| = {'number' => |
| Texinfo::TreeElement::new({'text' => $section_number}), |
| 'section_title' |
| => Texinfo::ManipulateTree::copy_element_tree($line_arg)}; |
| |
| if ($command->{'cmdname'} eq 'appendix' |
| and $command->{'extra'}->{'section_level'} == 1) { |
| $tree = $self->cdt('Appendix {number} {section_title}', |
| $substituted_strings); |
| } else { |
| # TRANSLATORS: numbered section title |
| $tree = $self->cdt('{number} {section_title}', |
| $substituted_strings); |
| } |
| } else { |
| $tree = $line_arg; |
| } |
| } |
| |
| $target->{'tree_nonumber'} = $line_arg; |
| } |
| $target->{'tree'} = $tree; |
| if ($in_code) { |
| $target->{'in_code'} = $in_code; |
| } |
| } |
| |
| return $target->{'tree_nonumber'}, $target->{'in_code'} |
| if ($no_number and exists($target->{'tree_nonumber'})); |
| return $target->{'tree'}, $target->{'in_code'}; |
| } |
| return undef, undef; |
| } |
| |
| sub _push_referred_command_stack_command($$) { |
| my ($self, $command) = @_; |
| |
| push @{$self->{'referred_command_stack'}}, $command; |
| } |
| |
| sub _pop_referred_command_stack($) { |
| my $self = shift; |
| |
| pop @{$self->{'referred_command_stack'}}; |
| } |
| |
| sub command_is_in_referred_command_stack($$) { |
| my ($self, $command) = @_; |
| |
| return grep {$_ eq $command} @{$self->{'referred_command_stack'}}; |
| } |
| |
| sub _convert_command_tree($$$$$$) { |
| my ($self, $command, $type, $selected_tree, $in_code, $command_info) = @_; |
| |
| my $explanation; |
| my $context_name; |
| |
| if (exists($command->{'cmdname'})) { |
| my $cmdname = $command->{'cmdname'}; |
| $context_name = $cmdname; |
| $explanation = "$command_info:$type \@$cmdname"; |
| } else { |
| $context_name = $command->{'type'}; |
| if ($command->{'type'} eq 'special_unit_element') { |
| my $special_unit_variety |
| = $command->{'associated_unit'}->{'special_unit_variety'}; |
| $explanation = "$command_info $special_unit_variety"; |
| } |
| } |
| |
| my $context_type = 0; |
| if ($type eq 'string' or $type eq 'string_nonumber') { |
| $context_type |= $CTXF_string; |
| } |
| if ($in_code) { |
| $context_type |= $CTXF_code; |
| } |
| |
| _new_document_context($self, $context_name, $context_type, $explanation); |
| |
| _set_multiple_conversions($self, undef); |
| |
| _push_referred_command_stack_command($self, $command); |
| |
| my $result = _convert($self, $selected_tree, $explanation); |
| |
| _pop_referred_command_stack($self); |
| |
| _unset_multiple_conversions($self); |
| |
| _pop_document_context($self); |
| return $result; |
| } |
| |
| sub _internal_command_text($$$) { |
| my ($self, $command, $type) = @_; |
| |
| my $target = _get_target($self, $command); |
| if (defined($target)) { |
| if (exists($target->{$type})) { |
| return $target->{$type}; |
| } |
| my ($command_tree, $in_code) = _internal_command_tree($self, $command, 0); |
| return '' if (!defined($command_tree)); |
| |
| my $selected_tree; |
| |
| if ($type =~ /^(.*)_nonumber$/ |
| and defined($target->{'tree_nonumber'})) { |
| $selected_tree = $target->{'tree_nonumber'}; |
| } else { |
| $selected_tree = $command_tree; |
| } |
| |
| $target->{$type} |
| = _convert_command_tree($self, $command, $type, $selected_tree, $in_code, |
| 'command_text'); |
| return $target->{$type}; |
| } |
| # Can happen |
| # * if USE_NODES is 0 and there are no sectioning commands. |
| # * if a special element target was set to undef in user defined code. |
| # * for @*ref with missing targets (maybe @novalidate needed in that case). |
| # * for @node header if the node consist only in spaces (example in sectioning |
| # in_menu_only_special_ascii_spaces_node). |
| # * for multiple targets with the same name, eg both @node and @anchor |
| # * with @inforef with node argument only, without manual argument. |
| return undef; |
| } |
| |
| # Return undef if there is no tree for the name |
| sub _internal_command_name_tree($$$) { |
| my ($self, $command, $no_number) = @_; |
| |
| my $target = _get_target($self, $command); |
| if (defined($target)) { |
| if (!exists($target->{'name_tree'})) { |
| my $tree; |
| if (exists($command->{'cmdname'}) |
| and $command->{'cmdname'} eq 'namedanchor' |
| and scalar(@{$command->{'contents'}}) > 1 |
| and not Texinfo::Common::empty_spaces_argument( |
| $command->{'contents'}->[1])) { |
| $tree = $command->{'contents'}->[1]; |
| } |
| $target->{'name_tree'} = $tree; |
| } |
| |
| # currently not possible |
| #return $target->{'name_tree_nonumber'} if ($no_number |
| # and $target->{'name_tree_nonumber'}); |
| return $target->{'name_tree'}, $target->{'in_code'}; |
| } |
| return undef, undef; |
| } |
| |
| sub _internal_command_name($$$) { |
| my ($self, $command, $type) = @_; |
| |
| my $name_type = "name_$type"; |
| |
| my $target = _get_target($self, $command); |
| if (defined($target)) { |
| if (exists($target->{$name_type})) { |
| return $target->{$name_type}; |
| } |
| my ($command_name_tree, $in_code) |
| = _internal_command_name_tree($self, $command, 0); |
| |
| if (!defined($command_name_tree)) { |
| ($command_name_tree, $in_code) |
| = _internal_command_tree($self, $command, 0); |
| } |
| return '' if (!defined($command_name_tree)); |
| |
| my $selected_tree; |
| |
| if ($type =~ /^(.*)_nonumber$/ |
| and exists($target->{'name_tree_nonumber'})) { |
| $selected_tree = $target->{'name_tree_nonumber'}; |
| } else { |
| $selected_tree = $command_name_tree; |
| } |
| |
| $target->{$name_type} |
| = _convert_command_tree($self, $command, $type, $selected_tree, $in_code, |
| 'command_name'); |
| return $target->{$name_type}; |
| } |
| return undef; |
| } |
| |
| # Return text to be used for $COMMAND description. |
| # $TYPE refers to the type of value returned from this function: |
| # 'text' - return text |
| # 'string' - return simpler text that can be used in element attributes |
| sub command_description($$;$) { |
| my ($self, $command, $type) = @_; |
| |
| if (!defined($type)) { |
| $type = 'text'; |
| } |
| |
| if (!defined($command)) { |
| cluck "in command_description($type) command not defined"; |
| } |
| |
| if (exists($command->{'extra'}) |
| and exists($command->{'extra'}->{'manual_content'})) { |
| return undef; |
| } |
| |
| my $target = _get_target($self, $command); |
| if (defined($target)) { |
| my $cached_type = 'description_'.${type}; |
| if (exists($target->{$cached_type})) { |
| return $target->{$cached_type}; |
| } |
| |
| if ((exists($command->{'type'}) |
| and $command->{'type'} eq 'special_unit_element') |
| or (exists($command->{'cmdname'}) |
| and ($command->{'cmdname'} eq 'anchor' |
| or $command->{'cmdname'} eq 'namedanchor' |
| or $command->{'cmdname'} eq 'float'))) { |
| $target->{$cached_type} = undef; |
| return undef; |
| } |
| my $node; |
| |
| if (exists($command->{'cmdname'})) { |
| if ($command->{'cmdname'} eq 'node') { |
| $node = $command; |
| } elsif (exists($self->{'document'})) { |
| my $sections_list = $self->{'document'}->sections_list(); |
| my $section_relations |
| = $sections_list->[$command->{'extra'}->{'section_number'} -1]; |
| if (exists($section_relations->{'associated_node'})) { |
| $node = $section_relations->{'associated_node'}->{'element'}; |
| } |
| } |
| } |
| |
| if (!defined($node) or !exists($node->{'extra'}) |
| or !$node->{'extra'}->{'node_number'} |
| or !exists($self->{'document'})) { |
| return undef; |
| } |
| |
| my $nodes_list = $self->{'document'}->nodes_list(); |
| my $node_relations = $nodes_list->[$node->{'extra'}->{'node_number'} -1]; |
| |
| my $node_description; |
| my $long_description = 0; |
| if (exists($node_relations->{'node_description'})) { |
| $node_description = $node_relations->{'node_description'}; |
| } elsif (exists($node_relations->{'node_long_description'})) { |
| $node_description = $node_relations->{'node_long_description'}; |
| $long_description = 1; |
| } else { |
| return undef; |
| } |
| |
| my $formatted_nodedescription_nr |
| = _formatted_nodedescription_nr($self, $node); |
| |
| my $cmdname = $command->{'cmdname'}; |
| my $context_name = "$cmdname description"; |
| my $explanation = "command_description:$type \@$cmdname"; |
| |
| my $description_element; |
| if (!$long_description) { |
| $description_element = $node_description->{'contents'}->[0]; |
| } else { |
| # nodedescriptionblock |
| $description_element = Texinfo::TreeElement::new( |
| {'contents' => $node_description->{'contents'}}); |
| } |
| my $multiple_formatted; |
| if ($formatted_nodedescription_nr > 1) { |
| $multiple_formatted |
| = 'node-description-'.$formatted_nodedescription_nr; |
| } |
| |
| my $context_type; |
| if ($type eq 'string') { |
| $context_type = $CTXF_string; |
| } |
| |
| $target->{$cached_type} |
| = $self->convert_tree_new_formatting_context($description_element, |
| $context_name, $context_type, |
| $multiple_formatted, $explanation); |
| |
| return $target->{$cached_type}; |
| } |
| return undef; |
| } |
| |
| sub get_special_unit_info_varieties($$) { |
| my ($self, $type) = @_; |
| |
| return keys(%{$self->{'special_unit_info'}->{$type}}); |
| } |
| |
| # Valid $SPECIAL_UNIT_VARIETY are the keys of |
| # %Texinfo::HTMLData::default_special_unit_info |
| sub special_unit_info($$$) { |
| my ($self, $type, $special_unit_variety) = @_; |
| |
| return $self->{'special_unit_info'}->{$type}->{$special_unit_variety}; |
| } |
| |
| # Currently the only possibility for $TYPE is heading |
| sub special_unit_info_text($$$;$) { |
| my ($self, $type, $special_unit_variety, $context) = @_; |
| |
| my $tree = $self->_special_unit_info_tree($type, |
| $special_unit_variety); |
| |
| return '' if (!defined($tree)); |
| |
| $context = 'normal' if (!defined($context)); |
| |
| my $explanation = "convert $special_unit_variety $type/$context"; |
| if ($context eq 'string') { |
| return $self->convert_tree_new_formatting_context($tree, $explanation, |
| $CTXF_string); |
| } else { |
| return $self->convert_tree($tree, $explanation); |
| } |
| } |
| |
| my %valid_direction_string_type = ( |
| # accesskey associated to the direction |
| 'accesskey' => 1, |
| # direction button name |
| 'button' => 1, |
| # description of the direction |
| 'description' => 1, |
| # section number corresponding to the example in About text |
| 'example' => 1, |
| # rel/ref string associated to the direction |
| 'rel' => 1, |
| # few words text associated to the direction |
| 'text' => 1, |
| ); |
| |
| my %valid_direction_string_context = ( |
| 'normal' => 1, |
| 'string' => 1, |
| ); |
| |
| my %direction_type_translation_context = ( |
| 'button' => 'button label', |
| 'description' => 'description', |
| 'text' => 'string', |
| ); |
| |
| sub direction_string($$$;$) { |
| my ($self, $direction, $string_type, $context) = @_; |
| |
| if (!exists($valid_direction_string_type{$string_type})) { |
| print STDERR "Incorrect type $string_type in direction_string call\n"; |
| return undef; |
| } |
| |
| $context = 'normal' if (!defined($context)); |
| |
| if (!exists($valid_direction_string_context{$context})) { |
| print STDERR "Incorrect context $context in direction_string call\n"; |
| return undef; |
| } |
| |
| $direction =~ s/^FirstInFile//; |
| |
| my $translated_directions_strings = $self->{'translated_direction_strings'}; |
| if (!defined($translated_directions_strings)) { |
| cluck(); |
| } |
| |
| if (not exists($self->{'directions_strings'}->{$string_type}->{$direction}) |
| or not exists($self->{'directions_strings'}->{$string_type} |
| ->{$direction}->{$context})) { |
| $self->{'directions_strings'}->{$string_type}->{$direction} = {} |
| if (not exists($self->{'directions_strings'} |
| ->{$string_type}->{$direction})); |
| if (exists($translated_directions_strings->{$string_type}) |
| # can exist and be undef if user-defined and also maybe for |
| # some default directions, but maybe only for unlikely type. |
| and defined($translated_directions_strings->{$string_type} |
| ->{$direction}) |
| and defined($translated_directions_strings->{$string_type} |
| ->{$direction}->{'converted'})) { |
| |
| # translate already converted direction strings |
| my $converted_directions |
| = $translated_directions_strings->{$string_type} |
| ->{$direction}->{'converted'}; |
| my $context_converted_string; |
| if (exists($converted_directions->{$context})) { |
| $context_converted_string = $converted_directions->{$context}; |
| } elsif ($context eq 'string' |
| and defined($converted_directions->{'normal'})) { |
| $context_converted_string = $converted_directions->{'normal'}; |
| } |
| if (defined($context_converted_string)) { |
| my $result_string |
| = $self->cdt_string($context_converted_string); |
| $self->{'directions_strings'}->{$string_type}->{$direction}->{$context} |
| = $self->substitute_html_non_breaking_space($result_string); |
| } else { |
| $self->{'directions_strings'}->{$string_type}->{$direction}->{$context} |
| = undef; |
| } |
| } elsif (exists($translated_directions_strings->{$string_type}) |
| # can exist and be undef if user-defined and also maybe for |
| # some default directions, but maybe only for unlikely type. |
| and defined($translated_directions_strings->{$string_type} |
| ->{$direction}) |
| and defined($translated_directions_strings->{$string_type} |
| ->{$direction}->{'to_convert'})) { |
| # translate direction strings that need to be translated and converted |
| my $translation_context = $direction; |
| $translation_context .= ' (current section)' if ($direction eq 'This'); |
| $translation_context .= ' direction ' |
| .$direction_type_translation_context{$string_type}; |
| my $translated_tree |
| = $self->pcdt($translation_context, |
| $translated_directions_strings->{$string_type} |
| ->{$direction}->{'to_convert'}); |
| |
| my $context_str = "DIRECTION $direction ($string_type/$context)"; |
| |
| my $context_type; |
| if ($context eq 'string') { |
| $context_type = $CTXF_string; |
| } |
| |
| my $result_string |
| = $self->convert_tree_new_formatting_context($translated_tree, |
| $context_str, $context_type, |
| undef, $context_str); |
| |
| # NOTE direction strings should be simple Texinfo code, but it is |
| # possible to set to anything through customization. Since |
| # anything except simple code is incorrect, there is no guarantee |
| # on the output, but it is good if there is no crash. |
| # If there is a @documentlanguage in $converted_tree, translate_names |
| # would be called and |
| # $self->{'directions_strings'}->{$string_type}->{$direction} would be |
| # reset. So, for this very special case (tested in the test suite), |
| # there may be a need to set again even though it was already done |
| # just above. |
| $self->{'directions_strings'}->{$string_type}->{$direction} = {} |
| if (not $self->{'directions_strings'}->{$string_type}->{$direction}); |
| |
| $self->{'directions_strings'}->{$string_type}->{$direction}->{$context} |
| = $result_string; |
| } else { |
| $self->{'directions_strings'}->{$string_type}->{$direction}->{$context} |
| = undef; |
| } |
| } |
| return $self->{'directions_strings'}->{$string_type} |
| ->{$direction}->{$context}; |
| } |
| |
| sub global_direction_unit($$) { |
| my ($self, $direction) = @_; |
| |
| return $self->{'global_units_directions'}->{$direction}; |
| } |
| |
| sub global_direction_text($$) { |
| my ($self, $direction) = @_; |
| |
| return $self->{'global_texts_directions'}->{$direction}; |
| } |
| |
| |
| |
| # API for misc conversion and formatting functions |
| |
| # if $OUTPUT_UNITS is defined, the first output unit is used if a proper |
| # top output unit is not found. |
| sub _get_top_unit($;$) { |
| my ($self, $output_units) = @_; |
| |
| my $global_commands; |
| if (exists($self->{'document'})) { |
| $global_commands = $self->{'document'}->global_commands_information(); |
| |
| if (defined($global_commands)) { |
| my $section_top = $global_commands->{'top'}; |
| if (defined($section_top)) { |
| return $section_top->{'associated_unit'}; |
| } |
| } |
| } |
| |
| my $node_top = $self->converter_find_identifier_target('Top'); |
| |
| if (defined($node_top)) { |
| if (!exists($node_top->{'associated_unit'})) { |
| die "No associated unit for node_top: " |
| .Texinfo::Common::debug_print_element($node_top, 1); |
| } |
| return $node_top->{'associated_unit'}; |
| } |
| |
| if (defined($output_units)) { |
| return $output_units->[0]; |
| } |
| return undef; |
| } |
| |
| # it is considered 'top' only if element corresponds to @top or |
| # element is a node |
| sub unit_is_top_output_unit($$) { |
| my ($self, $output_unit) = @_; |
| |
| my $top_output_unit = _get_top_unit($self); |
| if (defined($top_output_unit) and $top_output_unit eq $output_unit) { |
| return 1; |
| } else { |
| return 0; |
| } |
| } |
| |
| our %default_shared_conversion_states; |
| |
| sub _XS_get_shared_conversion_state($$$;@) { |
| my $self = shift; |
| my $cmdname = shift; |
| my $state_name = shift; |
| my @args = @_; |
| |
| return _get_shared_conversion_state($self, $cmdname, |
| $state_name, @args); |
| } |
| |
| sub _XS_set_shared_conversion_state($$$;@) { |
| my $self = shift; |
| my $cmdname = shift; |
| my $state_name = shift; |
| my @args = @_; |
| |
| _set_shared_conversion_state($self, $cmdname, |
| $state_name, @args); |
| } |
| |
| sub register_footnote($$$$$$$) { |
| my ($self, $command, $footid, $docid, $number_in_doc, |
| $footnote_location_filename, $multi_expanded_region) = @_; |
| |
| my $in_skipped_node_top |
| = $self->get_shared_conversion_state('top', 'in_skipped_node_top'); |
| if (!defined($in_skipped_node_top) or $in_skipped_node_top != 1) { |
| push @{$self->{'pending_footnotes'}}, [$command, $footid, $docid, |
| $number_in_doc, $footnote_location_filename, $multi_expanded_region]; |
| } |
| } |
| |
| # No equivalent function in C, the pending_footnotes are accessed directly |
| sub get_pending_footnotes($) { |
| my $self = shift; |
| |
| my @result = @{$self->{'pending_footnotes'}}; |
| @{$self->{'pending_footnotes'}} = (); |
| return \@result; |
| } |
| |
| |
| # API to register, cancel and get inline content that should be output |
| # when in an inline situation, mostly in a paragraph or preformatted |
| sub register_pending_formatted_inline_content($$$) { |
| my ($self, $category, $inline_content) = @_; |
| |
| if (!defined($inline_content)) { |
| return; |
| } |
| |
| if (not exists($self->{'pending_inline_content'})) { |
| $self->{'pending_inline_content'} = []; |
| } |
| push @{$self->{'pending_inline_content'}}, [$category, $inline_content]; |
| } |
| |
| # cancel only the first pending content for the category |
| sub cancel_pending_formatted_inline_content($$) { |
| my ($self, $category) = @_; |
| |
| if (exists($self->{'pending_inline_content'})) { |
| my $pending_inline = $self->{'pending_inline_content'}; |
| my $current_idx = scalar(@$pending_inline) - 1; |
| if ($current_idx >= 0) { |
| while ($current_idx >= 0) { |
| if ($pending_inline->[$current_idx]->[0] eq $category) { |
| my $removed = splice(@$pending_inline, $current_idx, 1); |
| return $removed->[1]; |
| } |
| $current_idx--; |
| } |
| } |
| } |
| return undef; |
| } |
| |
| sub get_pending_formatted_inline_content($) { |
| my $self = shift; |
| |
| if (not exists($self->{'pending_inline_content'})) { |
| return ''; |
| } else { |
| my $result = ''; |
| foreach my $category_inline_content (@{$self->{'pending_inline_content'}}) { |
| if (defined($category_inline_content->[1])) { |
| $result .= $category_inline_content->[1]; |
| } |
| } |
| delete $self->{'pending_inline_content'}; |
| return $result; |
| } |
| } |
| |
| # API to associate inline content to an element, typically |
| # paragraph or preformatted. Allows to associate the pending |
| # content to the first inline element. |
| sub associate_pending_formatted_inline_content($$$) { |
| my ($self, $element, $inline_content) = @_; |
| |
| $self->{'associated_inline_content'}->{$element} .= $inline_content; |
| } |
| |
| sub get_associated_formatted_inline_content($$) { |
| my ($self, $element) = @_; |
| |
| if (exists($self->{'associated_inline_content'}->{$element})) { |
| my $result = $self->{'associated_inline_content'}->{$element}; |
| delete $self->{'associated_inline_content'}->{$element}; |
| return $result; |
| } |
| return ''; |
| } |
| |
| # API to register an information to a file and get it. To be able to |
| # set an integer information during conversion and get it back during headers |
| # and footers conversion |
| sub register_file_information($$$) { |
| my ($self, $key, $value) = @_; |
| |
| if (!exists($self->{'current_filename'})) { |
| cluck(); |
| } |
| |
| $self->{'html_files_information'}->{$self->{'current_filename'}} = {} |
| if (!exists( |
| $self->{'html_files_information'}->{$self->{'current_filename'}})); |
| $self->{'html_files_information'}->{$self->{'current_filename'}}->{$key} |
| = $value; |
| } |
| |
| sub get_file_information($$;$) { |
| my ($self, $key, $filename) = @_; |
| |
| if (not defined($filename)) { |
| $filename = $self->{'current_filename'}; |
| } |
| if (not defined($filename) |
| or not exists($self->{'html_files_information'}) |
| or not exists($self->{'html_files_information'}->{$filename}) |
| or not exists($self->{'html_files_information'}->{$filename}->{$key})) { |
| return (0, undef); |
| } |
| return (1, $self->{'html_files_information'}->{$filename}->{$key}) |
| } |
| |
| sub current_filename($) { |
| my $self = shift; |
| |
| return $self->{'current_filename'}; |
| } |
| |
| sub current_output_unit($) { |
| my $self = shift; |
| |
| return $self->{'current_output_unit'}; |
| } |
| |
| # information from converter available 'read-only', set up before |
| # really starting the formatting. |
| my %available_converter_info; |
| foreach my $converter_info ('copying_comment', 'date_in_header', |
| 'destination_directory', 'document', 'document_name', |
| 'documentdescription_string', 'documentinfo_metadata', |
| 'expanded_formats', |
| 'jslicenses', 'line_break_element', 'non_breaking_space', |
| 'paragraph_symbol', 'simpletitle_command_name', 'simpletitle_tree', |
| 'title_string', 'title_tree', 'title_titlepage') { |
| $available_converter_info{$converter_info} = 1; |
| } |
| |
| sub get_info($$) { |
| my ($self, $converter_info) = @_; |
| |
| if (not exists($available_converter_info{$converter_info})) { |
| confess("BUG: $converter_info not an available converter info"); |
| } |
| if (defined($self->{'converter_info'}->{$converter_info})) { |
| return $self->{'converter_info'}->{$converter_info}; |
| } |
| return undef; |
| } |
| |
| sub _source_info_id($) { |
| my $source_info = shift; |
| |
| my $result; |
| if (exists($source_info->{'file_name'})) { |
| $result = $source_info->{'file_name'}; |
| } else { |
| $result = ''; |
| } |
| $result .= '-'; |
| if (exists($source_info->{'macro'})) { |
| $result .= $source_info->{'macro'}; |
| } |
| $result .= '-'; |
| if (exists($source_info->{'line_nr'})) { |
| $result .= $source_info->{'line_nr'}; |
| } else { |
| $result .= '0'; |
| } |
| return $result; |
| } |
| |
| sub _check_htmlxref_already_warned($$$) { |
| my ($self, $manual_name, $source_info) = @_; |
| |
| my $node_manual_key; |
| if (defined($source_info)) { |
| $node_manual_key = _source_info_id($source_info).'-'.$manual_name; |
| } else { |
| $node_manual_key = 'UNDEF-'.$manual_name; |
| } |
| if (exists($self->{'check_htmlxref_already_warned'}->{$node_manual_key})) { |
| return 1; |
| } else { |
| $self->{'check_htmlxref_already_warned'}->{$node_manual_key} = 1; |
| return 0; |
| } |
| } |
| |
| our %defaults; |
| |
| # heading |
| my %default_translated_special_unit_info |
| = %{ Texinfo::HTMLData::get_default_translated_special_unit_info() }; |
| |
| # for rel, see http://www.w3.org/TR/REC-html40/types.html#type-links |
| my %default_converted_directions_strings |
| = %{ Texinfo::HTMLData::get_default_converted_directions_strings() }; |
| |
| # translation contexts should be consistent with |
| # %direction_type_translation_context. If the direction is not used |
| # as is, it should also be taken into account in direction_string(). |
| # For now 'This' becomes 'This (current section)'. |
| my %default_translated_directions_strings |
| = %{ Texinfo::HTMLData::get_default_translated_directions_strings() }; |
| |
| our @no_args_commands_contexts; |
| |
| # to be called before starting conversion and when language changes. |
| # $RESET_CONTEXT is the context being set or reset. |
| # $REF_CONTEXT is the context that should be used for defaults, if |
| # defined and there is no information for $CMDNAME $RESET_CONTEXT. |
| # In that case, 'unset' should exist for $RESET_CONTEXT. |
| sub _reset_unset_no_arg_commands_formatting_context($$$$;$) { |
| my ($self, $cmdname, $reset_context, $ref_context, $translate) = @_; |
| |
| my $conversion_contexts = $self->{'no_arg_commands_formatting'}->{$cmdname}; |
| |
| # should never happen as unset is set at initialization in that case |
| #if (!exists($conversion_contexts->{$reset_context})) { |
| # die "Non-existing no_arg_commands_formatting $cmdname $reset_context"; |
| #} |
| my $no_arg_command_context = $conversion_contexts->{$reset_context}; |
| |
| if (defined($ref_context) and exists($no_arg_command_context->{'unset'})) { |
| foreach my $key (keys(%{$conversion_contexts->{$ref_context}})) { |
| # If present, both 'translated_converted' and (possibly translated) |
| # 'text' are referred to. |
| # In case of 'text', if 'translated_tree' is referred to and |
| # $translate is set, the 'text' will be replaced just below. |
| $no_arg_command_context->{$key} |
| = $conversion_contexts->{$ref_context}->{$key}; |
| } |
| } |
| if ($translate |
| and exists($conversion_contexts->{'translated_tree'}) |
| and not exists($no_arg_command_context->{'translated_converted'})) { |
| my $translated_tree |
| = $conversion_contexts->{'translated_tree'}; |
| my $translation_result; |
| my $explanation = "Translated NO ARG \@$cmdname ctx $reset_context"; |
| my $context_str = "Tr $cmdname ctx $reset_context"; |
| if ($reset_context eq 'normal') { |
| $translation_result |
| = $self->convert_tree($translated_tree, $explanation); |
| } elsif ($reset_context eq 'preformatted') { |
| # there does not seems to be anything simpler... |
| my $preformatted_cmdname = 'example'; |
| _new_document_context($self, $context_str); |
| _open_command_update_context($self, $preformatted_cmdname); |
| $translation_result |
| = $self->convert_tree($translated_tree, $explanation); |
| _convert_command_update_context($self, $preformatted_cmdname); |
| _pop_document_context($self); |
| } elsif ($reset_context eq 'string') { |
| _new_document_context($self, $context_str, $CTXF_string); |
| $translation_result = $self->convert_tree($translated_tree, |
| $explanation); |
| _pop_document_context($self); |
| } elsif ($reset_context eq 'css_string') { |
| $translation_result = _html_convert_css_string($self, $translated_tree, |
| $context_str); |
| } |
| $no_arg_command_context->{'text'} = $translation_result; |
| } |
| } |
| |
| sub _complete_no_arg_commands_formatting($$;$) { |
| my ($self, $cmdname, $translate) = @_; |
| |
| _reset_unset_no_arg_commands_formatting_context($self, $cmdname, |
| 'normal', undef, $translate); |
| _reset_unset_no_arg_commands_formatting_context($self, $cmdname, |
| 'preformatted', 'normal', $translate); |
| _reset_unset_no_arg_commands_formatting_context($self, $cmdname, |
| 'string', 'preformatted', $translate); |
| _reset_unset_no_arg_commands_formatting_context($self, $cmdname, |
| 'css_string', 'string', $translate); |
| } |
| |
| my @sorted_no_arg_commands; |
| |
| sub _translate_names($) { |
| my $self = shift; |
| |
| my $debug = $self->get_conf('DEBUG'); |
| |
| if ($debug) { |
| my $output_encoding_name = $self->get_conf('OUTPUT_ENCODING_NAME'); |
| $output_encoding_name = 'UNDEF' if (!defined($output_encoding_name)); |
| my $bcp47_locale = $self->current_bcp47_locale(); |
| print STDERR "\nTRANSLATE_NAMES encoding_name: $output_encoding_name" |
| ." bcp47_locale: $bcp47_locale\n"; |
| } |
| |
| # reset strings such that they are translated when needed. |
| # could also use the keys of $self->{'translated_direction_strings'} |
| foreach my $string_type (keys(%default_translated_directions_strings)) { |
| $self->{'directions_strings'}->{$string_type} = {}; |
| } |
| |
| # could also use keys of $self->{'translated_special_unit_info_texinfo'} |
| foreach my $type (keys(%default_translated_special_unit_info)) { |
| $self->{'translated_special_unit_info_tree'}->{$type} = {}; |
| } |
| |
| # delete the tree and formatted results for special elements |
| # such that they are redone with the new tree when needed. |
| foreach my $special_unit_variety |
| ($self->get_special_unit_info_varieties('direction')) { |
| my $special_unit_direction |
| = $self->special_unit_info('direction', $special_unit_variety); |
| my $special_unit |
| = $self->global_direction_unit($special_unit_direction); |
| if (defined($special_unit)) { |
| my $command = $special_unit->{'unit_command'}; |
| if (defined($command) |
| and exists($self->{'targets'}->{$command})) { |
| my $target = $self->{'targets'}->{$command}; |
| foreach my $key ('text', 'string', 'tree', 'description_text', |
| 'description_string') { |
| delete $target->{$key}; |
| } |
| } |
| } |
| } |
| my %translated_commands; |
| foreach my $cmdname (@sorted_no_arg_commands) { |
| my $conversion_contexts = $self->{'no_arg_commands_formatting'}->{$cmdname}; |
| |
| my $translated_tree; |
| if (exists($conversion_contexts->{'translated_to_convert'})) { |
| $translated_tree |
| = $self->cdt($conversion_contexts->{'translated_to_convert'}); |
| } else { |
| # generic Converter default translated commands |
| $translated_tree = $self->translated_command_tree($cmdname); |
| } |
| if (defined($translated_tree)) { |
| $conversion_contexts->{'translated_tree'} = $translated_tree; |
| $translated_commands{$cmdname} = 1; |
| } |
| foreach my $context (@no_args_commands_contexts) { |
| if (exists($conversion_contexts->{$context}->{'translated_converted'}) |
| and not exists($conversion_contexts->{$context}->{'unset'})) { |
| $translated_commands{$cmdname} = 1; |
| $conversion_contexts->{$context}->{'text'} |
| = $self->cdt_string( |
| $conversion_contexts->{$context}->{'translated_converted'}); |
| } |
| } |
| } |
| foreach my $cmdname (keys(%translated_commands)) { |
| _complete_no_arg_commands_formatting($self, $cmdname, 1); |
| } |
| |
| print STDERR "END TRANSLATE_NAMES\n\n" if ($debug); |
| } |
| |
| my %preformatted_commands_context = %preformatted_commands; |
| $preformatted_commands_context{'verbatim'} = 1; |
| |
| my %pre_class_commands; |
| foreach my $preformatted_command (keys(%preformatted_commands_context)) { |
| # no class for the @small* variants |
| if ($small_block_associated_command{$preformatted_command}) { |
| $pre_class_commands{$preformatted_command} |
| = $small_block_associated_command{$preformatted_command}; |
| } else { |
| $pre_class_commands{$preformatted_command} = $preformatted_command; |
| } |
| } |
| $pre_class_commands{'menu'} = 'menu'; |
| |
| our @informative_global_commands; |
| our @contents_commands; |
| |
| our %default_no_arg_commands_formatting; |
| |
| sub register_opened_section_level($$$$) { |
| my ($self, $filename, $level, $close_string) = @_; |
| |
| if (!exists($self->{'pending_closes'}->{$filename})) { |
| $self->{'pending_closes'}->{$filename} = []; |
| } |
| my $pending_closes = $self->{'pending_closes'}->{$filename}; |
| while (@$pending_closes < $level) { |
| push(@$pending_closes, ""); |
| } |
| push(@$pending_closes, $close_string); |
| } |
| |
| sub close_registered_sections_level($$$) { |
| my ($self, $filename, $level) = @_; |
| |
| if (not defined($level)) { |
| cluck 'close_registered_sections_level $level not defined'; |
| } |
| |
| my @closed_elements; |
| if (!exists($self->{'pending_closes'}->{$filename})) { |
| return \@closed_elements; |
| } |
| |
| my $pending_closes = $self->{'pending_closes'}->{$filename}; |
| while (@$pending_closes > $level) { |
| my $close_string = pop @$pending_closes; |
| push(@closed_elements, $close_string) |
| if ($close_string ne ""); |
| } |
| return \@closed_elements; |
| } |
| |
| |
| |
| # setting formatting context API |
| |
| # Following functions are sometimes called from converting functions, |
| # but mainly from within calls of convert_tree called from |
| # converting functions. If XS is used, this makes sure that the |
| # formatting contexts accessed and modified are only the formatting |
| # contexts in C data. |
| |
| # if $document_global_context is set, it means that the formatting |
| # is not done within the document formatting flow, but the formatted |
| # output may still end up in the document. In particular for |
| # command_text() which caches its computations. |
| sub _new_document_context($$;$$$) { |
| my ($self, $context, $context_type, $document_global_context, |
| $block_command) = @_; |
| |
| my $doc_context = |
| {'context' => $context, |
| 'formatting_context' => [{'context_name' => '_format'}], |
| 'composition_context' => [''], |
| 'preformatted_context' => [0], |
| 'inside_preformatted' => 0, |
| 'document_global_context' => $document_global_context, |
| 'block_commands' => [], |
| }; |
| |
| if (defined($document_global_context)) { |
| $self->{'document_global_context_counter'}++; |
| } |
| |
| if (defined($context_type) and ($context_type & $CTXF_code)) { |
| $doc_context->{'monospace'} = [1]; |
| } else { |
| $doc_context->{'monospace'} = [0]; |
| } |
| |
| if (defined($block_command)) { |
| push @{$doc_context->{'block_commands'}}, |
| $block_command; |
| } |
| |
| if (defined($context_type) and ($context_type & $CTXF_string)) { |
| $doc_context->{'string'}++; |
| } |
| push @{$self->{'document_context'}}, $doc_context; |
| } |
| |
| sub _pop_document_context($) { |
| my $self = shift; |
| |
| my $context = pop @{$self->{'document_context'}}; |
| if (defined($context->{'document_global_context'})) { |
| $self->{'document_global_context_counter'}--; |
| } |
| } |
| |
| sub _set_string_context($) { |
| my $self = shift; |
| |
| $self->{'document_context'}->[-1]->{'string'}++; |
| } |
| |
| sub _unset_string_context($) { |
| my $self = shift; |
| |
| $self->{'document_context'}->[-1]->{'string'}--; |
| } |
| |
| sub _set_raw_context($) { |
| my $self = shift; |
| |
| $self->{'document_context'}->[-1]->{'raw'}++; |
| } |
| |
| sub _unset_raw_context($) { |
| my $self = shift; |
| |
| $self->{'document_context'}->[-1]->{'raw'}--; |
| } |
| |
| sub _set_multiple_conversions($$) { |
| my ($self, $multiple_pass) = @_; |
| |
| push @{$self->{'multiple_pass'}}, $multiple_pass; |
| } |
| |
| sub _unset_multiple_conversions($) { |
| my $self = shift; |
| |
| pop @{$self->{'multiple_pass'}}; |
| } |
| |
| sub _open_command_update_context($$) { |
| my ($self, $command_name) = @_; |
| |
| my $convert_to_latex; |
| |
| if ((exists($brace_commands{$command_name}) |
| and ($brace_commands{$command_name} eq 'context' |
| or exists($math_commands{$command_name}))) |
| or $command_name eq 'printindex') { |
| _new_document_context($self, $command_name); |
| } |
| if (exists($format_context_commands{$command_name})) { |
| push @{$self->{'document_context'}->[-1]->{'formatting_context'}}, |
| {'context_name' => '@'.$command_name}; |
| } |
| if (exists($block_commands{$command_name})) { |
| push @{$self->{'document_context'}->[-1]->{'block_commands'}}, |
| $command_name; |
| } |
| my $preformatted = 0; |
| if (exists($pre_class_commands{$command_name})) { |
| push @{$self->{'document_context'}->[-1]->{'preformatted_classes'}}, |
| $pre_class_commands{$command_name}; |
| if (exists($preformatted_commands{$command_name})) { |
| $self->{'document_context'}->[-1]->{'inside_preformatted'}++; |
| $preformatted = 1; |
| } elsif ($block_commands{$command_name} eq 'menu' |
| and $self->{'document_context'}->[-1]->{'inside_preformatted'}) { |
| $preformatted = 1; |
| } |
| } |
| if (exists($composition_context_commands{$command_name})) { |
| push @{$self->{'document_context'}->[-1]->{'composition_context'}}, |
| $command_name; |
| push @{$self->{'document_context'}->[-1]->{'preformatted_context'}}, |
| $preformatted; |
| } |
| if (exists($format_raw_commands{$command_name})) { |
| $self->{'document_context'}->[-1]->{'raw'}++; |
| } elsif ($command_name eq 'verbatim') { |
| $self->{'document_context'}->[-1]->{'verbatim'}++; |
| } |
| if (exists($brace_code_commands{$command_name}) or |
| exists($preformatted_code_commands{$command_name})) { |
| push @{$self->{'document_context'}->[-1]->{'monospace'}}, 1; |
| } elsif (exists($brace_commands{$command_name}) |
| and $brace_commands{$command_name} eq 'style_no_code') { |
| push @{$self->{'document_context'}->[-1]->{'monospace'}}, 0; |
| } elsif ($self->{'upper_case_commands'}->{$command_name}) { |
| $self->{'document_context'}->[-1]->{'formatting_context'}->[-1] |
| ->{'upper_case'}++; |
| } elsif (exists($math_commands{$command_name})) { |
| $self->{'document_context'}->[-1]->{'math'}++; |
| $convert_to_latex = 1 if ($self->get_conf('CONVERT_TO_LATEX_IN_MATH')); |
| } |
| if ($command_name eq 'verb') { |
| $self->{'document_context'}->[-1]->{'formatting_context'}->[-1] |
| ->{'space_protected'}++; |
| } elsif ($command_name eq 'w') { |
| $self->{'document_context'}->[-1]->{'formatting_context'}->[-1] |
| ->{'no_break'}++; |
| } |
| return $convert_to_latex; |
| } |
| |
| sub _convert_command_update_context($$) { |
| my ($self, $command_name) = @_; |
| |
| if (exists($composition_context_commands{$command_name})) { |
| pop @{$self->{'document_context'}->[-1]->{'composition_context'}}; |
| pop @{$self->{'document_context'}->[-1]->{'preformatted_context'}}; |
| } |
| if (exists($pre_class_commands{$command_name})) { |
| pop @{$self->{'document_context'}->[-1]->{'preformatted_classes'}}; |
| if (exists($preformatted_commands{$command_name})) { |
| $self->{'document_context'}->[-1]->{'inside_preformatted'}--; |
| } |
| } |
| if (exists($preformatted_code_commands{$command_name}) |
| or (exists($brace_commands{$command_name}) |
| and $brace_commands{$command_name} eq 'style_no_code') |
| or exists($brace_code_commands{$command_name})) { |
| pop @{$self->{'document_context'}->[-1]->{'monospace'}}; |
| } elsif ($self->{'upper_case_commands'}->{$command_name}) { |
| $self->{'document_context'}->[-1]->{'formatting_context'}->[-1] |
| ->{'upper_case'}--; |
| } elsif (exists($math_commands{$command_name})) { |
| $self->{'document_context'}->[-1]->{'math'}--; |
| } |
| if ($command_name eq 'verb') { |
| $self->{'document_context'}->[-1]->{'formatting_context'}->[-1] |
| ->{'space_protected'}--; |
| } elsif ($command_name eq 'w') { |
| $self->{'document_context'}->[-1]->{'formatting_context'}->[-1] |
| ->{'no_break'}--; |
| } |
| if (exists($format_raw_commands{$command_name})) { |
| $self->{'document_context'}->[-1]->{'raw'}--; |
| } elsif ($command_name eq 'verbatim') { |
| $self->{'document_context'}->[-1]->{'verbatim'}--; |
| } |
| if (exists($block_commands{$command_name})) { |
| pop @{$self->{'document_context'}->[-1]->{'block_commands'}}; |
| } |
| if (exists($format_context_commands{$command_name})) { |
| pop @{$self->{'document_context'}->[-1]->{'formatting_context'}}; |
| } |
| if ((exists($brace_commands{$command_name}) |
| and ($brace_commands{$command_name} eq 'context' |
| or exists($math_commands{$command_name}))) |
| or $command_name eq 'printindex') { |
| _pop_document_context($self); |
| } |
| } |
| |
| sub _open_type_update_context($$) { |
| my ($self, $type_name) = @_; |
| |
| if ($type_name eq 'paragraph') { |
| $self->{'document_context'}->[-1]->{'formatting_context'}->[-1] |
| ->{'paragraph_number'}++; |
| } elsif ($type_name eq 'preformatted' |
| or $type_name eq 'rawpreformatted') { |
| $self->{'document_context'}->[-1]->{'formatting_context'}->[-1] |
| ->{'preformatted_number'}++; |
| } elsif ($self->{'pre_class_types'}->{$type_name}) { |
| push @{$self->{'document_context'}->[-1]->{'preformatted_classes'}}, |
| $self->{'pre_class_types'}->{$type_name}; |
| push @{$self->{'document_context'}->[-1]->{'preformatted_context'}}, 1; |
| push @{$self->{'document_context'}->[-1]->{'composition_context'}}, |
| $type_name; |
| } |
| |
| if ($self->{'code_types'}->{$type_name}) { |
| push @{$self->{'document_context'}->[-1]->{'monospace'}}, 1; |
| } |
| } |
| |
| sub _convert_type_update_context($$) { |
| my ($self, $type_name) = @_; |
| |
| if ($self->{'code_types'}->{$type_name}) { |
| pop @{$self->{'document_context'}->[-1]->{'monospace'}}; |
| } |
| if ($self->{'pre_class_types'}->{$type_name}) { |
| pop @{$self->{'document_context'}->[-1]->{'preformatted_classes'}}; |
| pop @{$self->{'document_context'}->[-1]->{'composition_context'}}; |
| pop @{$self->{'document_context'}->[-1]->{'preformatted_context'}}; |
| } |
| } |
| |
| sub _debug_print_html_contexts($) { |
| my $self = shift; |
| |
| my @document_contexts = map {defined($_->{'context'}) |
| ? $_->{'context'}: 'UNDEF'} |
| @{$self->{'document_context'}}; |
| my @contexts_names = map {defined($_->{'context_name'}) |
| ? $_->{'context_name'}: 'UNDEF'} |
| @{$self->{'document_context'}->[-1]->{'formatting_context'}}; |
| return "[".join('|',@document_contexts)."](".join('|',@contexts_names).")"; |
| } |
| |
| |
| |
| # converter API: converter_defaults, code called from converter_initialize, |
| # convert and output |
| |
| sub _set_variables_texi2html($) { |
| my $options = shift; |
| |
| my @texi2html_options = ( |
| ['SECTION_BUTTONS', ['FastBack', 'Back', 'Up', 'Forward', 'FastForward', |
| 'Space', 'Space', 'Space', 'Space', |
| 'Top', 'Contents', 'Index', 'About' ]], |
| ['TOP_BUTTONS', ['Back', 'Forward', 'Space', |
| 'Contents', 'Index', 'About']], |
| ['TOP_FOOTER_BUTTONS', ['Back', 'Forward', 'Space', |
| 'Contents', 'Index', 'About']], |
| |
| ['MISC_BUTTONS', [ 'Top', 'Contents', 'Index', 'About' ]], |
| ['CHAPTER_BUTTONS', [ 'FastBack', 'FastForward', 'Space', |
| 'Space', 'Space', 'Space', 'Space', |
| 'Top', 'Contents', 'Index', 'About', ]], |
| ['SECTION_FOOTER_BUTTONS', [ 'FastBack', 'FirstInFileBack', 'FirstInFileUp', |
| 'Forward', 'FastForward' ]], |
| ['CHAPTER_FOOTER_BUTTONS', [ 'FastBack', 'FastForward', 'Space', |
| 'Space', 'Space', 'Space', 'Space', |
| 'Top', 'Contents', 'Index', 'About', ]], |
| ['NODE_FOOTER_BUTTONS', [ 'FastBack', 'Back', |
| 'Up', 'Forward', 'FastForward', |
| 'Space', 'Space', 'Space', 'Space', |
| 'Top', 'Contents', 'Index', 'About' ]], |
| ); |
| my $regular_texi2html_options |
| = Texinfo::Options::get_regular_options('texi2html'); |
| foreach my $option (keys(%$regular_texi2html_options)) { |
| $options->{$option} = $regular_texi2html_options->{$option}; |
| } |
| foreach my $option (@texi2html_options) { |
| $options->{$option->[0]} = $option->[1]; |
| } |
| } |
| |
| sub converter_defaults($;$) { |
| my ($self, $conf) = @_; |
| |
| if (defined($conf) and $conf->{'TEXI2HTML'}) { |
| my $default_ref = { %defaults }; |
| my $texi2html_defaults = { %$default_ref }; |
| _set_variables_texi2html($texi2html_defaults); |
| return $texi2html_defaults; |
| } |
| return \%defaults; |
| } |
| |
| # called from converter_initialize |
| |
| our %htmlxref_entries; |
| |
| # $FILES is an array reference of file names binary strings. |
| sub _parse_htmlxref_files($$) { |
| my ($self, $files) = @_; |
| |
| my $htmlxref = {}; |
| |
| my $debug = $self->get_conf('DEBUG'); |
| |
| foreach my $file (@$files) { |
| my $fname = $file; |
| if ($self->get_conf('TEST')) { |
| my ($volume, $directories); |
| # strip directories for out-of-source builds reproducible file names |
| ($volume, $directories, $fname) = File::Spec->splitpath($file); |
| } |
| print STDERR "html refs config file: $file\n" if ($debug); |
| unless (open(HTMLXREF, $file)) { |
| my $htmlxref_file_name = $file; |
| my $encoding = $self->get_conf('COMMAND_LINE_ENCODING'); |
| if (defined($encoding)) { |
| $htmlxref_file_name = decode($encoding, $htmlxref_file_name); |
| } |
| $self->converter_document_warn( |
| sprintf(__("could not open html refs config file %s: %s"), |
| $htmlxref_file_name, $!)); |
| next; |
| } |
| my $line_nr = 0; |
| my %variables; |
| while (1) { |
| my $hline = <HTMLXREF>; |
| last if (!defined($hline)); |
| #my $line = $hline; |
| $line_nr++; |
| $hline =~ s/^\s*//; |
| next if $hline =~ /^#/; |
| next if $hline =~ /^$/; |
| chomp ($hline); |
| if ($hline =~ s/^(\w+)\s*=\s*//) { |
| # handle variables |
| my $var = $1; |
| my $re = join '|', map { quotemeta $_ } keys %variables; |
| $hline =~ s/\$\{($re)\}/defined $variables{$1} ? $variables{$1} |
| : "\${$1}"/ge; |
| $variables{$var} = $hline; |
| next; |
| } |
| my @htmlxref = split /\s+/, $hline; |
| my $manual = shift @htmlxref; |
| my $split_or_mono = shift @htmlxref; |
| #print STDERR "$fname: $line_nr: $manual $split_or_mono\n"; |
| if (!defined($split_or_mono)) { |
| $self->converter_line_warn(__("missing type"), |
| {'file_name' => $fname, 'line_nr' => $line_nr}); |
| next; |
| } elsif (!exists($htmlxref_entries{$split_or_mono})) { |
| $self->converter_line_warn(sprintf(__("unrecognized type: %s"), |
| $split_or_mono), |
| {'file_name' => $fname, 'line_nr' => $line_nr}); |
| next; |
| } |
| my $href = shift @htmlxref; |
| # No warning for an empty URL prefix as it is the only way to |
| # override an entry appearing in a file processed later on |
| #if (!defined($href)) { |
| # $self->converter_line_warn(sprintf( |
| # __("missing %s URL prefix for `%s'"), $split_or_mono, $manual), |
| # {'file_name' => $fname, 'line_nr' => $line_nr}); |
| #} |
| |
| # keep previously set value |
| next if (exists($htmlxref->{$manual}) |
| and exists($htmlxref->{$manual}->{$split_or_mono})); |
| |
| if (defined($href)) { # substitute 'variables' |
| my $re = join '|', map { quotemeta $_ } keys %variables; |
| $href =~ s/\$\{($re)\}/defined $variables{$1} ? $variables{$1} |
| : "\${$1}"/ge; |
| $href =~ s/\/*$// if ($split_or_mono ne 'mono'); |
| } else { |
| # Store empty text instead of undef, such that exists can safely |
| # be used. |
| $href = ''; |
| } |
| $htmlxref->{$manual} = {} if (!exists($htmlxref->{$manual})); |
| $htmlxref->{$manual}->{$split_or_mono} = $href; |
| } |
| if (!close (HTMLXREF)) { |
| my $htmlxref_file_name = $file; |
| my $encoding = $self->get_conf('COMMAND_LINE_ENCODING'); |
| if (defined($encoding)) { |
| $htmlxref_file_name = decode($encoding, $htmlxref_file_name); |
| } |
| $self->converter_document_warn(sprintf(__( |
| "error on closing html refs config file %s: %s"), |
| $htmlxref_file_name, $!)); |
| } |
| } |
| return $htmlxref; |
| } |
| |
| sub _load_htmlxref_files($) { |
| my $self = shift; |
| |
| my $deprecated_dirs = $self->{'deprecated_config_directories'}; |
| |
| my @htmlxref_files; |
| my $htmlxref_mode = $self->get_conf('HTMLXREF_MODE'); |
| return if (defined($htmlxref_mode) and $htmlxref_mode eq 'none'); |
| my $htmlxref_file_name = 'htmlxref.cnf'; |
| if (defined($htmlxref_mode) and $htmlxref_mode eq 'file') { |
| if (defined($self->get_conf('HTMLXREF_FILE'))) { |
| $htmlxref_file_name = $self->get_conf('HTMLXREF_FILE'); |
| } |
| my ($encoded_htmlxref_file_name, $htmlxref_file_encoding) |
| = $self->encoded_output_file_name($htmlxref_file_name); |
| if (-e $encoded_htmlxref_file_name and -r $encoded_htmlxref_file_name) { |
| @htmlxref_files = ($encoded_htmlxref_file_name); |
| } else { |
| $self->converter_document_warn( |
| sprintf(__("could not find html refs config file %s"), |
| $htmlxref_file_name)); |
| } |
| } else { |
| my @htmlxref_dirs; |
| if ($self->get_conf('TEST')) { |
| my $curdir = File::Spec->curdir(); |
| # to have reproducible tests, do not use system or user |
| # directories if TEST is set. |
| @htmlxref_dirs = join('/', ($curdir, '.texinfo')); |
| |
| if ($Texinfo::ModulePath::texinfo_uninstalled) { |
| unshift @htmlxref_dirs, join('/', ( |
| $Texinfo::ModulePath::t2a_srcdir, 'perl', 't', 'input_files')); |
| } |
| } elsif ($self->get_conf('TEXINFO_LANGUAGE_DIRECTORIES') |
| and scalar(@{$self->get_conf('TEXINFO_LANGUAGE_DIRECTORIES')}) > 0) { |
| @htmlxref_dirs = @{$self->get_conf('TEXINFO_LANGUAGE_DIRECTORIES')}; |
| } |
| |
| my $cnf_directory_name; |
| |
| # no htmlxref for tests, unless explicitly specified |
| if ($self->get_conf('TEST')) { |
| if (defined($self->get_conf('HTMLXREF_FILE'))) { |
| $htmlxref_file_name = $self->get_conf('HTMLXREF_FILE'); |
| } else { |
| $htmlxref_file_name = undef; |
| } |
| } else { |
| $cnf_directory_name = 'htmlxref.d'; |
| if (defined($self->get_conf('HTMLXREF_FILE'))) { |
| $htmlxref_file_name = $self->get_conf('HTMLXREF_FILE'); |
| } |
| } |
| |
| my ($encoded_htmlxref_file_name, $htmlxref_file_encoding); |
| # encode file name and handle specific cases for the main htmlxref file |
| # without search in directories. |
| if (defined($htmlxref_file_name)) { |
| ($encoded_htmlxref_file_name, $htmlxref_file_encoding) |
| = $self->encoded_output_file_name($htmlxref_file_name); |
| if (File::Spec->file_name_is_absolute($encoded_htmlxref_file_name)) { |
| if (-e $encoded_htmlxref_file_name and -r $encoded_htmlxref_file_name) { |
| push @htmlxref_files, $encoded_htmlxref_file_name; |
| } |
| $htmlxref_file_name = undef; |
| } else { |
| my ($volume, $path_directories, $file) |
| = File::Spec->splitpath($htmlxref_file_name); |
| my @path_directories = File::Spec->splitdir($path_directories); |
| # do not search in directories if the file name already contains |
| # directories. |
| if (scalar(@path_directories) > 0) { |
| if (-e $encoded_htmlxref_file_name |
| and -r $encoded_htmlxref_file_name) { |
| push @htmlxref_files, $encoded_htmlxref_file_name; |
| } |
| $htmlxref_file_name = undef; |
| } |
| } |
| } |
| |
| # now search in directories |
| if (defined($htmlxref_file_name) or defined($cnf_directory_name)) { |
| my ($encoded_cnf_directory_name, $cnf_directory_encoding); |
| if (defined($cnf_directory_name)) { |
| ($encoded_cnf_directory_name, $cnf_directory_encoding) |
| = $self->encoded_output_file_name($cnf_directory_name); |
| } |
| |
| my $deprecated_dirs_used; |
| foreach my $dir (@htmlxref_dirs) { |
| next unless (-d $dir); |
| my $deprecated_dir_set = 0; |
| if (defined($htmlxref_file_name)) { |
| my $possible_file = "$dir/$encoded_htmlxref_file_name"; |
| if (-e $possible_file and -r $possible_file) { |
| if (defined($deprecated_dirs) and $deprecated_dirs->{$dir}) { |
| $deprecated_dirs_used = [] if (!defined($deprecated_dirs_used)); |
| push @$deprecated_dirs_used, $dir; |
| $deprecated_dir_set = 1; |
| } |
| push (@htmlxref_files, $possible_file); |
| } |
| } |
| if (defined($cnf_directory_name)) { |
| my $cnf_dir = "$dir/$encoded_cnf_directory_name"; |
| if (-d $cnf_dir) { |
| my $file_found = 0; |
| # the internal simple quotes are for the case of spaces in $cnf_dir. |
| my @possible_files = glob("'$cnf_dir/*.cnf'"); |
| foreach my $possible_file (sort(@possible_files)) { |
| if (-e $possible_file and -r $possible_file) { |
| push (@htmlxref_files, $possible_file); |
| $file_found = 1; |
| } |
| } |
| if (!$deprecated_dir_set and $file_found |
| and $deprecated_dirs and $deprecated_dirs->{$dir}) { |
| $deprecated_dirs_used = [] if (!defined($deprecated_dirs_used)); |
| push @$deprecated_dirs_used, $dir; |
| $deprecated_dir_set = 1; |
| } |
| } |
| } |
| } |
| if (defined($deprecated_dirs_used)) { |
| foreach my $dir (@$deprecated_dirs_used) { |
| my $encoding = $self->get_conf('COMMAND_LINE_ENCODING'); |
| my ($dir_name, $replacement_dir); |
| if (defined($encoding)) { |
| $dir_name = decode($encoding, $dir); |
| $replacement_dir = decode($encoding, $deprecated_dirs->{$dir}) |
| } else { |
| $dir_name = $dir; |
| $replacement_dir = $deprecated_dirs->{$dir}; |
| } |
| $self->converter_document_warn(sprintf(__( |
| "%s directory is deprecated. Use %s instead"), |
| $dir_name, $replacement_dir)); |
| } |
| } |
| } |
| } |
| |
| $self->{'htmlxref'} = {}; |
| if (scalar(@htmlxref_files)) { |
| $self->{'htmlxref'} = _parse_htmlxref_files($self, |
| \@htmlxref_files); |
| } |
| } |
| |
| sub _process_css_file($$$) { |
| my ($self, $fh, $file) = @_; |
| |
| my $in_rules = 0; |
| my $in_comment = 0; |
| my $in_import = 0; |
| my $in_string = 0; |
| my $rules = []; |
| my $imports = []; |
| my $line_nr = 0; |
| # the rule is to assume utf-8. There could also be a BOM, and |
| # the Content-Type: HTTP header but it is not relevant here. |
| # https://developer.mozilla.org/en-US/docs/Web/CSS/@charset |
| my $input_perl_encoding = 'utf-8'; |
| while (1) { |
| my $input_line = <$fh>; |
| last if (!defined($input_line)); |
| my $line = Encode::decode($input_perl_encoding, $input_line); |
| $line_nr++; |
| if ($line_nr == 1) { |
| # should always be the first line |
| if ($line =~ /^\@charset *"([^"]+)" *; *$/) { |
| my $charset = $1; |
| my $Encode_encoding_object = find_encoding($charset); |
| if (defined($Encode_encoding_object)) { |
| my $perl_encoding = $Encode_encoding_object->name(); |
| if (defined($perl_encoding) and $perl_encoding ne '') { |
| $input_perl_encoding = $perl_encoding; |
| } |
| } |
| next; |
| } |
| } |
| #print STDERR "Line: $line"; |
| if ($in_rules) { |
| push @$rules, $line; |
| next; |
| } |
| my $text = ''; |
| while (1) { |
| #sleep 1; |
| #print STDERR "${text}!in_comment $in_comment in_rules $in_rules in_import $in_import in_string $in_string: $line"; |
| if ($in_comment) { |
| if ($line =~ s/^(.*?\*\/)//) { |
| $text .= $1; |
| $in_comment = 0; |
| } else { |
| push @$imports, $text . $line; |
| last; |
| } |
| } elsif (!$in_string and $line =~ s/^\///) { |
| if ($line =~ s/^\*//) { |
| $text .= '/*'; |
| $in_comment = 1; |
| } else { |
| push (@$imports, $text. "\n") if ($text ne ''); |
| push (@$rules, '/' . $line); |
| $in_rules = 1; |
| last; |
| } |
| } elsif (!$in_string and $in_import and $line =~ s/^([\"\'])//) { |
| # strings outside of import start rules |
| $text .= "$1"; |
| $in_string = quotemeta("$1"); |
| } elsif ($in_string and $line =~ s/^(\\$in_string)//) { |
| $text .= $1; |
| } elsif ($in_string and $line =~ s/^($in_string)//) { |
| $text .= $1; |
| $in_string = 0; |
| } elsif ((! $in_string and !$in_import) |
| and ($line =~ s/^([\\]?\@import)$// |
| or $line =~ s/^([\\]?\@import\s+)//)) { |
| $text .= $1; |
| $in_import = 1; |
| } elsif (!$in_string and $in_import and $line =~ s/^\;//) { |
| $text .= ';'; |
| $in_import = 0; |
| } elsif (($in_import or $in_string) and $line =~ s/^(.)//) { |
| $text .= $1; |
| } elsif (!$in_import and $line =~ s/^([^\s])//) { |
| push (@$imports, $text. "\n") if ($text ne ''); |
| push (@$rules, $1 . $line); |
| $in_rules = 1; |
| last; |
| } elsif ($line =~ s/^(\s)//) { |
| $text .= $1; |
| } elsif ($line eq '') { |
| push (@$imports, $text); |
| last; |
| } |
| } |
| } |
| $self->converter_line_warn(__("string not closed in css file"), |
| {'file_name' => $file, 'line_nr' => $line_nr}) if ($in_string); |
| $self->converter_line_warn(__("--css-include ended in comment"), |
| {'file_name' => $file, 'line_nr' => $line_nr}) if ($in_comment); |
| $self->converter_line_warn(__("\@import not finished in css file"), |
| {'file_name' => $file, 'line_nr' => $line_nr}) |
| if ($in_import and !$in_comment and !$in_string); |
| return ($imports, $rules); |
| } |
| |
| sub _prepare_css($) { |
| my $self = shift; |
| |
| $self->{'files_css_rule_lines'} = []; |
| $self->{'files_css_import_lines'} = []; |
| |
| return if ($self->get_conf('NO_CSS')); |
| |
| my @css_import_lines; |
| my @css_rule_lines; |
| |
| my $css_files = $self->get_conf('CSS_FILES'); |
| foreach my $css_file (@$css_files) { |
| my $css_file_fh; |
| my $css_file_path; |
| if ($css_file eq '-') { |
| $css_file_fh = \*STDIN; |
| $css_file_path = '-'; |
| } else { |
| $css_file_path = Texinfo::Common::locate_include_file($css_file, |
| $self->get_conf('INCLUDE_DIRECTORIES')); |
| unless (defined($css_file_path)) { |
| my $css_input_file_name; |
| my $encoding = $self->get_conf('COMMAND_LINE_ENCODING'); |
| if (defined($encoding)) { |
| $css_input_file_name = decode($encoding, $css_file); |
| } else { |
| $css_input_file_name = $css_file; |
| } |
| $self->converter_document_warn(sprintf( |
| __("CSS file %s not found"), $css_input_file_name)); |
| next; |
| } |
| unless (open(CSSFILE, $css_file_path)) { |
| my $css_file_name; |
| my $encoding = $self->get_conf('COMMAND_LINE_ENCODING'); |
| if (defined($encoding)) { |
| $css_file_name = decode($encoding, $css_file_path); |
| } else { |
| $css_file_name = $css_file_path; |
| } |
| $self->converter_document_warn(sprintf(__( |
| "could not open --include-file %s: %s"), |
| $css_file_name, $!)); |
| next; |
| } |
| $css_file_fh = \*CSSFILE; |
| } |
| my ($import_lines, $rules_lines); |
| ($import_lines, $rules_lines) |
| = _process_css_file($self, $css_file_fh, $css_file_path); |
| if (!close($css_file_fh)) { |
| my $css_file_name; |
| my $encoding = $self->get_conf('COMMAND_LINE_ENCODING'); |
| if (defined($encoding)) { |
| $css_file_name = decode($encoding, $css_file_path); |
| } else { |
| $css_file_name = $css_file_path; |
| } |
| $self->converter_document_warn( |
| sprintf(__("error on closing CSS file %s: %s"), |
| $css_file_name, $!)); |
| } |
| push @css_import_lines, @$import_lines; |
| push @css_rule_lines, @$rules_lines; |
| |
| } |
| if ($self->get_conf('DEBUG')) { |
| if (@css_import_lines) { |
| print STDERR "# css import lines\n"; |
| foreach my $line (@css_import_lines) { |
| print STDERR "$line"; |
| } |
| } |
| if (@css_rule_lines) { |
| print STDERR "# css rule lines\n"; |
| foreach my $line (@css_rule_lines) { |
| print STDERR "$line"; |
| } |
| } |
| } |
| |
| push @{$self->{'files_css_import_lines'}}, @css_import_lines; |
| push @{$self->{'files_css_rule_lines'}}, @css_rule_lines; |
| } |
| |
| sub _html_converter_initialize_beginning($) { |
| my $self = shift; |
| |
| # used in initialization. Set if undef |
| if (!defined($self->get_conf('FORMAT_MENU'))) { |
| $self->force_conf('FORMAT_MENU', ''); |
| } |
| |
| # NOTE we reset silently if the split specification is not one known. |
| # The main program warns if the specific command line option value is |
| # not known. We could add a warning here to catch mistakes in init |
| # files. Wait for user reports. |
| my $split = $self->get_conf('SPLIT'); |
| if ($split and $split ne 'chapter' |
| and $split ne 'section' |
| and $split ne 'node') { |
| $self->force_conf('SPLIT', 'node'); |
| } |
| |
| my $max_header_level = $self->get_conf('MAX_HEADER_LEVEL'); |
| if (!defined($max_header_level)) { |
| $self->force_conf('MAX_HEADER_LEVEL', $defaults{'MAX_HEADER_LEVEL'}); |
| } elsif ($max_header_level < 1) { |
| $self->force_conf('MAX_HEADER_LEVEL', 1); |
| } |
| |
| # For CONTENTS_OUTPUT_LOCATION |
| # should lead to contents not output, but if not, it is not an issue, |
| # the way to set contents to be output or not should be through the |
| # contents and shortcontents @-commands and customization options. |
| foreach my $conf ('CONTENTS_OUTPUT_LOCATION', 'INDEX_ENTRY_COLON', |
| 'MENU_ENTRY_COLON') { |
| if (!defined($self->get_conf($conf))) { |
| $self->force_conf($conf, ''); |
| } |
| } |
| |
| _load_htmlxref_files($self); |
| _prepare_css($self); |
| } |
| |
| sub _XS_html_converter_get_customization($$$$$$$$$$$$$$$$$$$) { |
| } |
| |
| # conversion with convert and output and the functions they call |
| |
| # transform <hr> to <hr/> |
| sub _xhtml_re_close_lone_element($) { |
| my $element = shift; |
| |
| if ($element =~ /\/\s*>$/) { |
| # already a closed lone element |
| return $element; |
| } |
| $element =~ s/^(<[a-zA-Z][^<>]*)>$/$1\/>/; |
| return $element; |
| } |
| |
| my $xml_named_entity_nbsp = ' '; |
| |
| my %special_characters = ( |
| 'paragraph_symbol' => ['¶', '00B6'], |
| 'left_quote' => ['‘', '2018'], |
| 'right_quote' => ['’', '2019'], |
| 'bullet' => ['•', '2022'], |
| 'non_breaking_space' => [$xml_named_entity_nbsp, '00A0'], |
| ); |
| |
| our %default_commands_conversion; |
| our %default_css_element_class_styles; |
| |
| # This function initializes states that are initialized either in XS |
| # or in Perl. In general called by output or convert, as early as possible. |
| # May also be called on a converter when output and convert are not used, |
| # which happens only in tests. |
| # NOTE not called directly by convert_tree, which means that convert_tree |
| # needs to be called from a converter which would have had this function |
| # called already. |
| # |
| # $CONTEXT is the first conversion context name. |
| # $DOCUMENT is the converted Texinfo parsed document. |
| sub conversion_initialization($$;$) { |
| my ($self, $context, $document) = @_; |
| |
| $self->{'converter_info'} = {}; |
| |
| $self->set_document($document); |
| |
| if (defined($document)) { |
| $self->{'converter_info'}->{'document'} = $document; |
| } elsif ($self->get_conf('DEBUG')) { |
| print STDERR "REMARK: conversion_initialization: no document\n"; |
| } |
| |
| # needed if a converter is reused |
| $self->set_global_document_commands('before', \@informative_global_commands); |
| $self->set_global_document_commands('before', \@contents_commands); |
| |
| $self->{'css_import_lines'} = [@{$self->{'files_css_import_lines'}}]; |
| $self->{'css_rule_lines'} = [@{$self->{'files_css_rule_lines'}}]; |
| |
| $self->{'shared_conversion_state'} = {}; |
| |
| $self->{'document_context'} = []; |
| $self->{'document_global_context_counter'} = 0; |
| |
| $self->{'associated_inline_content'} = {}; |
| |
| foreach my $cmdname (keys(%default_shared_conversion_states)) { |
| foreach my $state_name |
| (keys(%{$default_shared_conversion_states{$cmdname}})) { |
| $self->define_shared_conversion_state($cmdname, $state_name, |
| $default_shared_conversion_states{$cmdname}->{$state_name}); |
| } |
| } |
| |
| # even if there is no actual file, this is needed if the API is used. |
| $self->{'html_files_information'} = {}; |
| |
| # Needed for CSS gathering, even if nothing related to CSS is output |
| $self->{'document_global_context_css'} = {}; |
| $self->{'page_css'} = {}; |
| |
| # targets |
| |
| # used for diverse tree elements: nodes and sectioning commands, indices, |
| # footnotes, special output units elements... |
| $self->{'targets'} = {}; |
| |
| # for footnotes |
| $self->{'special_targets'} = {'footnote_location' => {}}; |
| |
| $self->{'seen_ids'} = {}; |
| |
| # other |
| $self->{'pending_footnotes'} = []; |
| $self->{'pending_closes'} = {}; |
| |
| delete $self->{'current_node'}; |
| delete $self->{'current_root_command'}; |
| |
| # for user-defined translation results. Always reset such as not |
| # to get a cached translation obtained for a previous conversion. |
| $self->{'translation_cache'} = {}; |
| |
| my %special_characters_set; |
| |
| my $output_encoding = $self->get_conf('OUTPUT_ENCODING_NAME'); |
| |
| foreach my $special_character (keys(%special_characters)) { |
| my ($default_entity, $unicode_point) |
| = @{$special_characters{$special_character}}; |
| if ($self->get_conf('OUTPUT_CHARACTERS') |
| and Texinfo::Convert::Unicode::unicode_point_decoded_in_encoding( |
| $output_encoding, $unicode_point)) { |
| $special_characters_set{$special_character} |
| = charnames::vianame("U+$unicode_point"); |
| } elsif ($self->get_conf('USE_NUMERIC_ENTITY')) { |
| $special_characters_set{$special_character} |
| = '&#'.hex($unicode_point).';'; |
| } else { |
| $special_characters_set{$special_character} = $default_entity; |
| } |
| } |
| |
| $self->{'converter_info'}->{'non_breaking_space'} |
| = $special_characters_set{'non_breaking_space'}; |
| |
| $self->{'converter_info'}->{'paragraph_symbol'} |
| = $special_characters_set{'paragraph_symbol'}; |
| |
| if (not defined($self->get_conf('OPEN_QUOTE_SYMBOL'))) { |
| my $set = $self->set_conf('OPEN_QUOTE_SYMBOL', |
| $special_characters_set{'left_quote'}); |
| # override undef set in init file/command line |
| $self->force_conf('OPEN_QUOTE_SYMBOL', '') if (!$set); |
| } |
| if (not defined($self->get_conf('CLOSE_QUOTE_SYMBOL'))) { |
| my $set = $self->set_conf('CLOSE_QUOTE_SYMBOL', |
| $special_characters_set{'right_quote'}); |
| # override undef set in init file/command line |
| $self->force_conf('CLOSE_QUOTE_SYMBOL', '') if (!$set); |
| } |
| if (not defined($self->get_conf('MENU_SYMBOL'))) { |
| my $set = $self->set_conf('MENU_SYMBOL', |
| $special_characters_set{'bullet'}); |
| # override undef set in init file/command line |
| $self->force_conf('MENU_SYMBOL', '') if (!$set); |
| } |
| |
| my $line_break_element; |
| if ($self->get_conf('USE_XML_SYNTAX')) { |
| foreach my $customization_variable ('BIG_RULE', 'DEFAULT_RULE') { |
| my $variable_value = $self->get_conf($customization_variable); |
| if (defined($variable_value)) { |
| my $closed_lone_element = _xhtml_re_close_lone_element($variable_value); |
| if ($closed_lone_element ne $variable_value) { |
| $self->force_conf($customization_variable, $closed_lone_element); |
| } |
| } |
| } |
| $line_break_element = '<br/>'; |
| } else { |
| $line_break_element = '<br>'; |
| } |
| $self->{'converter_info'}->{'line_break_element'} = $line_break_element; |
| |
| # duplicate such as not to modify the defaults |
| my $conf_default_no_arg_commands_formatting_normal |
| = Storable::dclone($default_no_arg_commands_formatting{'normal'}); |
| |
| @sorted_no_arg_commands |
| = sort(keys(%{$default_no_arg_commands_formatting{'normal'}})); |
| |
| my $non_breaking_space = $self->get_info('non_breaking_space'); |
| |
| if ($non_breaking_space ne $xml_named_entity_nbsp) { |
| foreach my $space_command (' ', "\t", "\n", 'tie') { |
| $conf_default_no_arg_commands_formatting_normal->{$space_command}->{'text'} |
| = $non_breaking_space; |
| } |
| } |
| |
| if ($self->get_conf('USE_NUMERIC_ENTITY')) { |
| foreach my $cmdname (keys(%Texinfo::Convert::Unicode::unicode_entities)) { |
| $conf_default_no_arg_commands_formatting_normal->{$cmdname}->{'text'} |
| = $Texinfo::Convert::Unicode::unicode_entities{$cmdname}; |
| } |
| } |
| |
| $conf_default_no_arg_commands_formatting_normal->{'*'}->{'text'} |
| = $self->get_info('line_break_element'); |
| |
| # NOTE need to be before the call to css_set_selector_style just below |
| %{$self->{'css_element_class_styles'}} = %default_css_element_class_styles; |
| |
| # initialized here and not with the converter because it may depend on |
| # the document encoding. |
| $self->{'no_arg_commands_formatting'} = {}; |
| # use sorted for reproducible debug output |
| foreach my $cmdname (@sorted_no_arg_commands) { |
| $self->{'no_arg_commands_formatting'}->{$cmdname} = {}; |
| # For now, there are no 'translated_to_convert' Texinfo code for |
| # no args commands in the default case. The translated command |
| # is common to diverse output formats |
| if (defined($self->{'customized_no_arg_commands_formatting'} |
| ->{$cmdname}->{'translated_to_convert'})) { |
| $self->{'no_arg_commands_formatting'}->{$cmdname} |
| ->{'translated_to_convert'} |
| = $self->{'customized_no_arg_commands_formatting'} |
| ->{$cmdname}->{'translated_to_convert'}; |
| } |
| foreach my $context (@no_args_commands_contexts) { |
| my $no_arg_command_customized_formatting |
| = $self->{'customized_no_arg_commands_formatting'} |
| ->{$cmdname}->{$context}; |
| if (defined($no_arg_command_customized_formatting)) { |
| $self->{'no_arg_commands_formatting'}->{$cmdname}->{$context} |
| = $no_arg_command_customized_formatting; |
| } else { |
| my $context_default_default_no_arg_commands_formatting; |
| if ($context eq 'normal') { |
| $context_default_default_no_arg_commands_formatting |
| = $conf_default_no_arg_commands_formatting_normal; |
| } else { |
| $context_default_default_no_arg_commands_formatting |
| = $default_no_arg_commands_formatting{$context}; |
| } |
| if (defined($context_default_default_no_arg_commands_formatting |
| ->{$cmdname})) { |
| if ($self->get_conf('OUTPUT_CHARACTERS') |
| and Texinfo::Convert::Unicode::brace_no_arg_command( |
| $cmdname, $self->get_conf('OUTPUT_ENCODING_NAME'))) { |
| $self->{'no_arg_commands_formatting'}->{$cmdname}->{$context} |
| = { 'text' => Texinfo::Convert::Unicode::brace_no_arg_command( |
| $cmdname, $self->get_conf('OUTPUT_ENCODING_NAME'))}; |
| # reset CSS for itemize command arguments |
| if ($context eq 'css_string' |
| and exists($brace_commands{$cmdname}) |
| and $cmdname ne 'bullet' and $cmdname ne 'w' |
| and not $special_list_mark_css_string_no_arg_command{$cmdname}) { |
| my $css_string |
| = $self->{'no_arg_commands_formatting'} |
| ->{$cmdname}->{$context}->{'text'}; |
| $css_string = '"'.$css_string.'"'; |
| |
| css_set_selector_style($self, "ul.mark-$cmdname", |
| "list-style-type: $css_string"); |
| } |
| } else { |
| $self->{'no_arg_commands_formatting'}->{$cmdname}->{$context} |
| = $context_default_default_no_arg_commands_formatting->{$cmdname}; |
| } |
| } else { |
| # not possible for the normal context because the commands come |
| # from the normal context defaults, and in addition there should |
| # be normal context defaults for all the nobrace brace commands |
| # except for @today |
| #if ($context eq 'normal') { |
| # die "Brace no args command normal no set for $cmdname"; |
| #} |
| $self->{'no_arg_commands_formatting'}->{$cmdname}->{$context} |
| = {'unset' => 1}; |
| } |
| } |
| } |
| } |
| |
| # set sane defaults in case there is none and the default formatting |
| # function is used |
| foreach my $cmdname (@sorted_no_arg_commands) { |
| if (exists($self->{'commands_conversion'}->{$cmdname}) |
| and $self->{'commands_conversion'}->{$cmdname} |
| eq $default_commands_conversion{$cmdname}) { |
| _complete_no_arg_commands_formatting($self, $cmdname); |
| } |
| } |
| |
| # for global directions always set, and for directions to special elements, |
| # only filled if special elements are actually used. |
| $self->{'global_units_directions'} = {}; |
| |
| # three types of direction strings: |
| # * strings not translated, already converted |
| # * strings translated |
| # - strings already converted |
| # - strings not already converted |
| $self->{'directions_strings'} = {}; |
| |
| # The strings not translated, already converted are |
| # initialized here and not with the converter because |
| # substitute_html_non_breaking_space is used and it depends on the document. |
| foreach my $string_type (keys(%default_converted_directions_strings)) { |
| $self->{'directions_strings'}->{$string_type} = {}; |
| foreach my $direction (keys(%{$self->{'all_directions'}})) { |
| $self->{'directions_strings'}->{$string_type}->{$direction} = {}; |
| my $string_contexts; |
| if (exists($self->{'customized_direction_strings'}->{$string_type}) |
| and exists($self->{'customized_direction_strings'}->{$string_type} |
| ->{$direction})) { |
| if (defined($self->{'customized_direction_strings'}->{$string_type} |
| ->{$direction}->{'converted'})) { |
| $string_contexts |
| = $self->{'customized_direction_strings'}->{$string_type} |
| ->{$direction}->{'converted'}; |
| } else { |
| $string_contexts = {'normal' => undef }; |
| } |
| } else { |
| my $string |
| = $default_converted_directions_strings{$string_type}->{$direction}; |
| $string_contexts = {'normal' => $string}; |
| } |
| $string_contexts->{'string'} = $string_contexts->{'normal'} |
| if (not defined($string_contexts->{'string'})); |
| foreach my $context (keys(%$string_contexts)) { |
| if (defined($string_contexts->{$context})) { |
| $self->{'directions_strings'}->{$string_type} |
| ->{$direction}->{$context} |
| = $self->substitute_html_non_breaking_space( |
| $string_contexts->{$context}); |
| } else { |
| $self->{'directions_strings'}->{$string_type} |
| ->{$direction}->{$context} = undef; |
| } |
| } |
| } |
| } |
| |
| # direction strings |
| foreach my $string_type (keys(%default_translated_directions_strings)) { |
| # those will be determined from translatable strings |
| $self->{'directions_strings'}->{$string_type} = {}; |
| }; |
| |
| # to avoid infinite recursions when a section refers to itself, possibly |
| # indirectly |
| $self->{'referred_command_stack'} = []; |
| |
| $self->{'check_htmlxref_already_warned'} = {}; |
| |
| $self->{'converter_info'}->{'expanded_formats'} |
| = $self->{'expanded_formats'}; |
| |
| $self->{'multiple_pass'} = []; |
| |
| if (not defined($self->get_conf('NODE_NAME_IN_INDEX'))) { |
| $self->set_conf('NODE_NAME_IN_INDEX', $self->get_conf('USE_NODES')); |
| } |
| |
| if ($self->get_conf('HTML_MATH') |
| and not defined($self->get_conf('CONVERT_TO_LATEX_IN_MATH'))) { |
| $self->set_conf('CONVERT_TO_LATEX_IN_MATH', 1); |
| } |
| |
| if ($self->get_conf('CONVERT_TO_LATEX_IN_MATH')) { |
| $self->{'options_latex_math'} |
| = { Texinfo::Convert::LaTeX::copy_options_for_convert_to_latex_math($self) }; |
| } |
| |
| if ($self->get_conf('NO_TOP_NODE_OUTPUT') |
| and not defined($self->get_conf('SHOW_TITLE'))) { |
| $self->set_conf('SHOW_TITLE', 1); |
| } |
| |
| my $use_accesskey = $self->get_conf('USE_ACCESSKEY'); |
| if (!defined($use_accesskey) and $self->get_conf('SPLIT') |
| and $self->get_conf('SPLIT') eq 'node') { |
| $self->set_conf('USE_ACCESSKEY', 1); |
| } |
| |
| _new_document_context($self, $context); |
| } |
| |
| sub conversion_finalization($) { |
| my $self = shift; |
| |
| _pop_document_context($self); |
| } |
| |
| sub _prepare_title_titlepage($$$$) { |
| my ($self, $output_file, $output_filename, $output_units) = @_; |
| |
| # set file name to be the first file name for formatting of title page. |
| # The title page prepared here is thus only fit to be used in the first |
| # output unit. |
| if ($output_file ne '') { |
| $self->{'current_filename'} |
| = $output_units->[0]->{'unit_filename'}; |
| } else { |
| $self->{'current_filename'} = $output_filename; |
| } |
| |
| # title |
| $self->{'converter_info'}->{'title_titlepage'} |
| = &{$self->formatting_function('format_title_titlepage')}($self); |
| delete $self->{'current_filename'}; |
| } |
| |
| sub _html_convert_convert($$$$) { |
| my ($self, $document, $output_units, $special_units) = @_; |
| |
| my $result = ''; |
| |
| $self->{'current_filename'} = ''; |
| |
| my $unit_nr = 0; |
| # NOTE there is no rule before the footnotes special element in |
| # case of separate footnotes in this setting. |
| my $debug = $self->get_conf('DEBUG'); |
| foreach my $output_unit (@$output_units, @$special_units) { |
| print STDERR "\nUNIT $unit_nr\n" if ($debug); |
| my $output_unit_text = $self->convert_output_unit($output_unit, |
| "convert unit $unit_nr"); |
| $result .= $output_unit_text; |
| $unit_nr++; |
| } |
| delete $self->{'current_filename'}; |
| return $result; |
| } |
| |
| sub _prepare_simpletitle($) { |
| my $self = shift; |
| |
| if (exists($self->{'document'})) { |
| my $global_commands = $self->{'document'}->global_commands_information(); |
| if (defined($global_commands)) { |
| foreach my $simpletitle_command ('settitle', 'shorttitlepage') { |
| if (exists($global_commands->{$simpletitle_command})) { |
| my $command = $global_commands->{$simpletitle_command}; |
| next if (!exists($command->{'contents'}->[0]->{'contents'})); |
| $self->{'converter_info'}->{'simpletitle_tree'} |
| = $command->{'contents'}->[0]; |
| $self->{'converter_info'}->{'simpletitle_command_name'} |
| = $simpletitle_command; |
| last; |
| } |
| } |
| } |
| } |
| } |
| |
| # Common to output and convert, run after the first handler in output. |
| sub _init_conversion_after_setup_handler($) { |
| my $self = shift; |
| |
| # the presence of contents elements in the document is used in diverse |
| # places, set it once for all here |
| my @contents_elements_options |
| = grep {Texinfo::Common::valid_customization_option($_)} |
| sort(keys(%contents_command_special_unit_variety)); |
| $self->set_global_document_commands('last', \@contents_elements_options); |
| |
| # cache, as it is checked for each text element |
| if ($self->get_conf('OUTPUT_CHARACTERS') |
| and $self->get_conf('OUTPUT_ENCODING_NAME') |
| and $self->get_conf('OUTPUT_ENCODING_NAME') eq 'utf-8') { |
| $self->{'use_unicode_text'} = 1; |
| } |
| } |
| |
| sub _setup_convert($) { |
| my $self = shift; |
| |
| _init_conversion_after_setup_handler($self); |
| } |
| |
| # Conversion to a string, mostly used in tests. |
| # $SELF is the output converter object of class Texinfo::Convert::HTML (this |
| # module), and $DOCUMENT is the parsed document from the parser and structuring |
| sub convert($$) { |
| my ($self, $document) = @_; |
| |
| $self->conversion_initialization('_convert', $document); |
| |
| _setup_convert($self); |
| |
| my ($output_units, $special_units, $associated_special_units) |
| = _prepare_conversion_units($self, $document, undef); |
| |
| # setup global targets. It is not clearly relevant to have those |
| # global targets when called as convert, but the Top global |
| # unit directions is often referred to in code, so at least this |
| # global target needs to be setup. |
| # Since the relative directions are not set, this leads to lone |
| # global direction buttons such as [Contents] or [Index] appearing |
| # in otherwise empty navigation headings if those global directions |
| # are set and present in the buttons, as is the case in the default |
| # buttons. For example in converters_tests/ref_in_sectioning |
| # or converters_tests/sections_and_printindex. |
| _prepare_output_units_global_targets($self, $output_units, |
| $special_units, |
| $associated_special_units); |
| |
| # setup untranslated strings |
| _translate_names($self); |
| |
| _prepare_simpletitle($self); |
| |
| # title. Not often set in the default case, as convert() is only |
| # used in the *.t tests, and a title requires both simpletitle_tree |
| # and SHOW_TITLE set, with the default formatting function. |
| _prepare_title_titlepage($self, '', '', $output_units); |
| |
| # main conversion here |
| my $result = _html_convert_convert($self, $document, $output_units, |
| $special_units); |
| |
| $self->conversion_finalization(); |
| return $result; |
| } |
| |
| # Get the name of a file containing a label, as well as the identifier within |
| # that file to link to that label. $normalized is the normalized label name |
| # and $label_element is the label contents element. Labels are typically |
| # associated to @node, @*anchor or @float and to external nodes. |
| sub _normalized_label_id_file($$$) { |
| my ($self, $normalized, $label_element) = @_; |
| |
| my $target; |
| if (!defined($normalized) and defined($label_element)) { |
| $normalized |
| = Texinfo::Convert::NodeNameNormalization::convert_to_node_identifier( |
| $label_element); |
| } |
| |
| if (defined($normalized)) { |
| $target = _normalized_to_id($normalized); |
| } else { |
| $target = ''; |
| } |
| # to find out the Top node, one could check $normalized |
| if (defined($self->{'file_id_setting'}->{'label_target_name'})) { |
| $target = &{$self->{'file_id_setting'}->{'label_target_name'}}($self, |
| $normalized, $label_element, $target); |
| } |
| |
| my $filename = $self->node_information_filename($normalized, |
| $label_element); |
| |
| return ($filename, $target); |
| } |
| |
| sub _register_id($$) { |
| my ($self, $id) = @_; |
| |
| $self->{'seen_ids'}->{$id} = 1; |
| } |
| |
| sub _id_is_registered($$) { |
| my ($self, $id) = @_; |
| |
| if (exists($self->{'seen_ids'}->{$id})) { |
| return 1; |
| } else { |
| return 0; |
| } |
| } |
| |
| sub _unique_target($$) { |
| my ($self, $target_base) = @_; |
| |
| my $nr=1; |
| my $target = $target_base; |
| while (_id_is_registered($self, $target)) { |
| $target = $target_base.'-'.$nr; |
| $nr++; |
| # Avoid integer overflow |
| die if ($nr == 0); |
| } |
| return $target; |
| } |
| |
| sub _register_special_unit($$) { |
| my ($self, $special_unit_variety) = @_; |
| |
| my $special_unit = {'unit_type' => 'special_unit', |
| 'special_unit_variety' => $special_unit_variety, |
| 'directions' => {}}; |
| |
| # a "virtual" out of tree element used for targets |
| my $unit_command |
| = Texinfo::TreeElement::new({'type' => 'special_unit_element', |
| 'associated_unit' => $special_unit}); |
| $special_unit->{'unit_command'} = $unit_command; |
| |
| return $special_unit; |
| } |
| |
| # prepare both special output units in separate output units, and |
| # special output units associated to a regular document output unit, |
| # output as part of regular output but also possible target of |
| # special output unit direction. In practice, only contents and |
| # shortcontents are associated with special output unit directions |
| # and can be output as part of document output units. |
| sub _prepare_special_units($$) { |
| my ($self, $output_units) = @_; |
| |
| my $global_commands; |
| my $sections_list; |
| if (exists($self->{'document'})) { |
| $global_commands = $self->{'document'}->global_commands_information(); |
| $sections_list = $self->{'document'}->sections_list(); |
| } |
| |
| # for separate special output units |
| my %do_special; |
| # for associated special output units |
| my $associated_special_units = []; |
| if (defined($sections_list) and scalar(@{$sections_list}) > 1) { |
| foreach my $cmdname ('shortcontents', 'contents') { |
| my $special_unit_variety |
| = $contents_command_special_unit_variety{$cmdname}; |
| if ($self->get_conf($cmdname)) { |
| my $contents_location = $self->get_conf('CONTENTS_OUTPUT_LOCATION'); |
| if ($contents_location eq 'separate_element') { |
| $do_special{$special_unit_variety} = 1; |
| } else { |
| my $associated_output_unit; |
| if ($contents_location eq 'after_title') { |
| $associated_output_unit = $output_units->[0]; |
| } elsif ($contents_location eq 'after_top') { |
| if (defined($global_commands) |
| and exists($global_commands->{'top'})) { |
| my $section_top = $global_commands->{'top'}; |
| if (exists($section_top->{'associated_unit'})) { |
| $associated_output_unit = $section_top->{'associated_unit'}; |
| } |
| } |
| next unless ($associated_output_unit); |
| } elsif ($contents_location eq 'inline') { |
| if (defined($global_commands) |
| and exists($global_commands->{$cmdname})) { |
| foreach my $command(@{$global_commands->{$cmdname}}) { |
| my $root_command; |
| ($associated_output_unit, $root_command) |
| = _html_get_tree_root_element($self, $command); |
| if (defined($associated_output_unit)) { |
| last; |
| } |
| } |
| } else { |
| next; |
| } |
| } else { |
| # only happens with an unknown CONTENTS_OUTPUT_LOCATION |
| next; |
| } |
| my $special_unit = _register_special_unit($self, $special_unit_variety); |
| $special_unit->{'associated_document_unit'} = $associated_output_unit; |
| push @$associated_special_units, $special_unit; |
| } |
| } |
| } |
| } |
| |
| if (defined($global_commands) and exists($global_commands->{'footnote'}) |
| and scalar(@$output_units) > 1) { |
| my $footnotestyle = $self->get_conf('footnotestyle'); |
| if (defined($footnotestyle) and $footnotestyle eq 'separate') { |
| $do_special{'footnotes'} = 1; |
| } |
| } |
| |
| if ((!defined($self->get_conf('DO_ABOUT')) |
| and scalar(@$output_units) > 1 |
| and ($self->get_conf('SPLIT') or $self->get_conf('HEADERS'))) |
| or ($self->get_conf('DO_ABOUT'))) { |
| $do_special{'about'} = 1; |
| } |
| |
| my $special_units = []; |
| # sort special elements according to their index order from |
| # special_unit_info 'order'. |
| # First reverse the hash, using arrays in case some elements are at the |
| # same index, and sort to get alphabetically sorted special element |
| # varieties that are at the same index. |
| my %special_units_indices; |
| foreach my $special_unit_variety |
| (sort($self->get_special_unit_info_varieties('order'))) { |
| next unless ($do_special{$special_unit_variety}); |
| my $index = $self->special_unit_info('order', $special_unit_variety); |
| $special_units_indices{$index} = [] |
| if (not exists($special_units_indices{$index})); |
| push @{$special_units_indices{$index}}, $special_unit_variety; |
| } |
| # now sort according to indices |
| my @sorted_elements_varieties; |
| foreach my $index (sort { $a <=> $b } (keys(%special_units_indices))) { |
| push @sorted_elements_varieties, @{$special_units_indices{$index}}; |
| } |
| |
| |
| # Setup separate special output units |
| my $previous_output_unit; |
| $previous_output_unit = $output_units->[-1]; |
| |
| foreach my $special_unit_variety (@sorted_elements_varieties) { |
| |
| my $special_unit = _register_special_unit($self, $special_unit_variety); |
| |
| push @$special_units, $special_unit; |
| |
| if (defined($previous_output_unit)) { |
| $special_unit->{'tree_unit_directions'} = {}; |
| $previous_output_unit->{'tree_unit_directions'} = {} |
| if (not exists($previous_output_unit->{'tree_unit_directions'})); |
| $special_unit->{'tree_unit_directions'}->{'prev'} = $previous_output_unit; |
| $previous_output_unit->{'tree_unit_directions'}->{'next'} = $special_unit; |
| } |
| $previous_output_unit = $special_unit; |
| } |
| |
| return $special_units, $associated_special_units; |
| } |
| |
| sub _new_sectioning_command_target($$) { |
| my ($self, $command) = @_; |
| |
| my ($normalized_name, $filename) |
| = $self->normalized_sectioning_command_filename($command); |
| |
| my $target_base = _normalized_to_id($normalized_name); |
| if ($target_base !~ /\S/ and $command->{'cmdname'} eq 'top') { |
| # @top is allowed to be empty. In that case it gets this target name |
| $target_base = 'SEC_Top'; |
| $normalized_name = $target_base; |
| } |
| my $nr=1; |
| my $target = $target_base; |
| if ($target_base ne '') { |
| $target = _unique_target($self, $target_base); |
| } else { |
| $target = ''; |
| } |
| |
| # These are undefined if the $target is set to ''. |
| my $target_contents; |
| my $target_shortcontents; |
| if (exists($sectioning_heading_commands{$command->{'cmdname'}})) { |
| if ($target ne '') { |
| my $target_base_contents = 'toc-'.$normalized_name; |
| $target_contents = _unique_target($self, $target_base_contents); |
| |
| my $target_base_shortcontents = 'stoc-'.$normalized_name; |
| $target_shortcontents |
| = _unique_target($self, $target_base_shortcontents); |
| } |
| } |
| |
| if (defined($self->{'file_id_setting'}->{'sectioning_command_target_name'})) { |
| ($target, $target_contents, |
| $target_shortcontents, $filename) |
| = &{$self->{'file_id_setting'}->{'sectioning_command_target_name'}}($self, |
| $command, $target, |
| $target_contents, |
| $target_shortcontents, |
| $filename); |
| } |
| if ($self->get_conf('DEBUG')) { |
| print STDERR "Register $command->{'cmdname'} $target\n"; |
| } |
| $self->{'targets'}->{$command} = { |
| 'target' => $target, |
| 'section_filename' => $filename, |
| }; |
| _register_id($self, $target); |
| if (defined($target_contents)) { |
| $self->{'targets'}->{$command}->{'contents_target'} = $target_contents; |
| _register_id($self, $target_contents); |
| } else { |
| $self->{'targets'}->{$command}->{'contents_target'} = ''; |
| } |
| if (defined($target_shortcontents)) { |
| $self->{'targets'}->{$command}->{'shortcontents_target'} |
| = $target_shortcontents; |
| _register_id($self, $target_shortcontents); |
| } else { |
| $self->{'targets'}->{$command}->{'shortcontents_target'} = ''; |
| } |
| } |
| |
| sub _set_special_units_targets_files($$$) { |
| my ($self, $special_units, $document_name) = @_; |
| |
| my $extension = ''; |
| $extension = $self->get_conf('EXTENSION') |
| if (defined($self->get_conf('EXTENSION'))); |
| |
| my $debug = $self->get_conf('DEBUG'); |
| foreach my $special_unit (@$special_units) { |
| |
| my $special_unit_variety = $special_unit->{'special_unit_variety'}; |
| |
| # it may be undef'ined in user customization code |
| my $target |
| = $self->special_unit_info('target', $special_unit_variety); |
| next if (!defined($target)); |
| my $default_filename; |
| if ($self->get_conf('SPLIT') or !$self->get_conf('MONOLITHIC') |
| # in general $document_name not defined means called through convert |
| and defined($document_name)) { |
| my $special_unit_file_string = |
| $self->special_unit_info('file_string', $special_unit_variety); |
| $special_unit_file_string = '' if (!defined($special_unit_file_string)); |
| $default_filename = $document_name . $special_unit_file_string; |
| $default_filename .= '.'.$extension if (defined($extension)); |
| } else { |
| $default_filename = undef; |
| } |
| |
| my $filename; |
| if (defined($self->{'file_id_setting'}->{'special_unit_target_file_name'})) { |
| ($target, $filename) |
| = &{$self->{'file_id_setting'}->{'special_unit_target_file_name'}}( |
| $self, |
| $special_unit, |
| $target, |
| $default_filename); |
| } |
| $filename = $default_filename if (!defined($filename)); |
| |
| if ($debug) { |
| my $fileout = $filename; |
| $fileout = 'UNDEF' if (!defined($fileout)); |
| print STDERR 'Add special' |
| # uncomment for the perl object name |
| #." $special_unit" |
| ." $special_unit_variety: target $target,\n". |
| " filename $fileout\n"; |
| } |
| |
| my $unit_command = $special_unit->{'unit_command'}; |
| $self->{'targets'}->{$unit_command} = {'target' => $target, |
| 'special_unit_filename' => $filename, |
| }; |
| _register_id($self, $target); |
| } |
| } |
| |
| sub _prepare_associated_special_units_targets($$) { |
| my ($self, $associated_output_units) = @_; |
| |
| return unless (defined($associated_output_units)); |
| |
| my $debug = $self->get_conf('DEBUG'); |
| foreach my $special_unit (@$associated_output_units) { |
| my $special_unit_variety = $special_unit->{'special_unit_variety'}; |
| |
| # it may be undef'ined in user customization code |
| my $target |
| = $self->special_unit_info('target', $special_unit_variety); |
| |
| my $default_filename; |
| |
| my $filename; |
| if (defined($self->{'file_id_setting'}->{'special_unit_target_file_name'})) { |
| ($target, $filename) |
| = &{$self->{'file_id_setting'}->{'special_unit_target_file_name'}}( |
| $self, |
| $special_unit, |
| $target, |
| $default_filename); |
| } |
| $filename = $default_filename if (!defined($filename)); |
| if ($debug) { |
| my $str_filename = $filename; |
| $str_filename = 'UNDEF (default)' if (not defined($str_filename)); |
| my $str_target = $target; |
| $str_target = 'UNDEF' if (not defined($str_target)); |
| print STDERR 'Add content' |
| # uncomment to get the perl object name |
| #." $special_unit" |
| ." $special_unit_variety: target $str_target,\n". |
| " filename $str_filename\n"; |
| } |
| |
| my $unit_command = $special_unit->{'unit_command'}; |
| my $command_target = {'target' => $target}; |
| $self->{'targets'}->{$unit_command} = $command_target; |
| if (defined($target)) { |
| _register_id($self, $target); |
| } |
| if (defined ($filename)) { |
| $command_target->{'special_unit_filename'} |
| = $filename; |
| } |
| } |
| } |
| |
| # This set with two different codes |
| # * the target information, id and normalized filename of 'identifiers_target', |
| # ie everything that may be the target of a ref: @node, @float label, |
| # @anchor, @namedanchor. |
| # * The target information of sectioning elements |
| # @node and section commands targets are therefore both set. |
| # |
| # conversion to HTML is done on-demand, upon call to command_text |
| # and similar functions. |
| # Note that 'node_filename', which is set here for Top target information |
| # too, is not used later for Top anchors or links, see the NOTE below |
| # associated with setting TOP_NODE_FILE_TARGET. |
| sub _set_root_commands_targets_node_files($) { |
| my $self = shift; |
| |
| my $sections_list; |
| my $labels_list; |
| if (exists($self->{'document'})) { |
| $sections_list = $self->{'document'}->sections_list(); |
| $labels_list = $self->{'document'}->labels_list(); |
| } |
| |
| if (defined($labels_list)) { |
| my $debug = $self->get_conf('DEBUG'); |
| my $extension = ''; |
| $extension = '.'.$self->get_conf('EXTENSION') |
| if (defined($self->get_conf('EXTENSION')) |
| and $self->get_conf('EXTENSION') ne ''); |
| |
| foreach my $target_element (@$labels_list) { |
| next if (not exists($target_element->{'extra'}) |
| or not $target_element->{'extra'}->{'is_target'}); |
| my $label_element = Texinfo::Common::get_label_element($target_element); |
| my ($node_filename, $target) |
| = _normalized_label_id_file($self, $target_element->{'extra'} |
| ->{'identifier'}, |
| $label_element); |
| $node_filename .= $extension; |
| if (defined($self->{'file_id_setting'}->{'node_file_name'})) { |
| # a non defined filename is ok if called with convert, but not |
| # if output in files. We reset if undef, silently unless verbose |
| # in case called by convert. |
| my $user_node_filename |
| = &{$self->{'file_id_setting'}->{'node_file_name'}}( |
| $self, $target_element, $node_filename); |
| if (defined($user_node_filename)) { |
| $node_filename = $user_node_filename; |
| } elsif ($self->get_conf('VERBOSE')) { |
| $self->converter_document_warn(sprintf(__( |
| "user-defined node file name not set for `%s'"), |
| $node_filename)); |
| } elsif ($debug) { |
| warn "user-defined node file name undef for `$node_filename'\n"; |
| } |
| } |
| if ($debug) { |
| print STDERR 'Label' |
| # uncomment to get the perl object names |
| #."($target_element)" |
| ." \@$target_element->{'cmdname'} $target, $node_filename\n"; |
| } |
| $self->{'targets'}->{$target_element} = {'target' => $target, |
| 'node_filename' => $node_filename}; |
| _register_id($self, $target); |
| } |
| } |
| |
| if (defined($sections_list)) { |
| foreach my $section_relations (@{$sections_list}) { |
| my $section_element = $section_relations->{'element'}; |
| _new_sectioning_command_target($self, $section_element); |
| } |
| } |
| } |
| |
| sub _set_heading_commands_targets($) { |
| my $self = shift; |
| |
| my $global_commands; |
| if (exists($self->{'document'})) { |
| $global_commands = $self->{'document'}->global_commands_information(); |
| } |
| if (defined($global_commands)) { |
| foreach my $cmdname (sort(keys(%sectioning_heading_commands)), |
| 'xrefname') { |
| if (!exists($root_commands{$cmdname}) |
| and exists($global_commands->{$cmdname})) { |
| foreach my $command (@{$global_commands->{$cmdname}}) { |
| _new_sectioning_command_target($self, $command); |
| } |
| } |
| } |
| } |
| } |
| |
| sub _prepare_index_entries_targets($) { |
| my $self = shift; |
| |
| my $indices_information; |
| if (exists($self->{'document'})) { |
| $indices_information = $self->{'document'}->indices_information(); |
| } |
| |
| if (defined($indices_information)) { |
| foreach my $index_name (sort(keys(%$indices_information))) { |
| foreach my $index_entry (@{$indices_information->{$index_name} |
| ->{'index_entries'}}) { |
| my $main_entry_element = $index_entry->{'entry_element'}; |
| # does not refer to the document |
| my $seeentry |
| = Texinfo::Common::index_entry_referred_entry($main_entry_element, |
| 'seeentry'); |
| next if (defined($seeentry)); |
| my $seealso |
| = Texinfo::Common::index_entry_referred_entry($main_entry_element, |
| 'seealso'); |
| next if (defined($seealso)); |
| |
| my $region = ''; |
| $region = "$main_entry_element->{'extra'}->{'element_region'}-" |
| if (defined($main_entry_element->{'extra'}->{'element_region'})); |
| my $entry_reference_content_element |
| = Texinfo::Indices::converter_index_content_element( |
| $main_entry_element, $self, 1); |
| # construct element to convert to a normalized identifier to use as |
| # hrefs target |
| my $normalize_index_element = Texinfo::TreeElement::new( |
| {'contents' => [$entry_reference_content_element]}); |
| |
| my $subentries_tree |
| = Texinfo::Convert::Utils::comma_index_subentries_tree( |
| $main_entry_element, ' '); |
| |
| if (defined($subentries_tree)) { |
| push @{$normalize_index_element->{'contents'}}, |
| @{$subentries_tree->{'contents'}}; |
| } |
| |
| my $normalized_index = |
| Texinfo::Convert::NodeNameNormalization::convert_to_identifier( |
| $normalize_index_element); |
| my $target_base = "index-" . $region .$normalized_index; |
| my $target = _unique_target($self, $target_base); |
| _register_id($self, $target); |
| my $target_element = $main_entry_element; |
| $target_element = $index_entry->{'entry_associated_element'} |
| if ($index_entry->{'entry_associated_element'}); |
| $self->{'targets'}->{$target_element} = {'target' => $target, }; |
| } |
| } |
| } |
| } |
| |
| sub _prepare_footnotes_targets($) { |
| my $self = shift; |
| |
| my $footid_base = 'FOOT'; |
| my $docid_base = 'DOCF'; |
| |
| my $global_commands; |
| if (exists($self->{'document'})) { |
| $global_commands = $self->{'document'}->global_commands_information(); |
| } |
| |
| if (defined($global_commands) and exists($global_commands->{'footnote'})) { |
| my $debug = $self->get_conf('DEBUG'); |
| my $footnote_nr = 0; |
| foreach my $footnote (@{$global_commands->{'footnote'}}) { |
| $footnote_nr++; |
| my $nr = $footnote_nr; |
| # anchor for the footnote text |
| my $footid = $footid_base.$nr; |
| # anchor for the location of the @footnote in the document |
| my $docid = $docid_base.$nr; |
| while (_id_is_registered($self, $docid) |
| or _id_is_registered($self, $footid)) { |
| $nr++; |
| $footid = $footid_base.$nr; |
| $docid = $docid_base.$nr; |
| # Avoid integer overflow |
| die if ($nr == 0); |
| } |
| _register_id($self, $footid); |
| _register_id($self, $docid); |
| $self->{'targets'}->{$footnote} = { 'target' => $footid }; |
| $self->{'special_targets'}->{'footnote_location'}->{$footnote} |
| = { 'target' => $docid }; |
| print STDERR 'Enter footnote' |
| # uncomment for the perl object name |
| #." $footnote" |
| .": target $footid, nr $footnote_nr\n" |
| .Texinfo::Convert::Texinfo::convert_to_texinfo($footnote)."\n" |
| if ($debug); |
| } |
| } |
| } |
| |
| # $DOCUMENT is a Texinfo document with a parsed tree. Return a list of the |
| # "elements" we need to output in the HTML file(s). Each "element" is what |
| # can go in one HTML file, such as the content between @node lines in the |
| # Texinfo source. |
| # Also setup targets associated to tree elements and to elements associated |
| # to special units. |
| sub _prepare_conversion_units($$$) { |
| my ($self, $document, $document_name) = @_; |
| |
| my ($output_units, $special_units, $associated_special_units); |
| |
| if ($self->get_conf('USE_NODES')) { |
| $output_units = Texinfo::OutputUnits::split_by_node($document); |
| } else { |
| $output_units = Texinfo::OutputUnits::split_by_section($document); |
| } |
| |
| # Needs to be set early in case it would be needed to find some region |
| # command associated root command. |
| $self->{'document_units'} = $output_units; |
| |
| # configuration used to determine if a special element is to be done |
| # (in addition to contents) |
| my @conf_for_special_units = ('footnotestyle'); |
| $self->set_global_document_commands('last', \@conf_for_special_units); |
| # NOTE if the last value of footnotestyle is separate, all the footnotes |
| # formatted text are set to the special element set in _prepare_special_units |
| # as _html_get_tree_root_element uses the Footnote direction for every |
| # footnote. Therefore if @footnotestyle separate is set late in the |
| # document the current value may not be consistent with the link obtained |
| # for the footnote formatted text. This is not an issue, as the manual |
| # says that @footnotestyle should only appear in the preamble, and it |
| # makes sense to have something consistent in the whole document for |
| # footnotes position. |
| ($special_units, $associated_special_units) |
| = _prepare_special_units($self, $output_units); |
| # reset to the default |
| $self->set_global_document_commands('before', \@conf_for_special_units); |
| |
| # Do that before the other elements, to be sure that special page ids |
| # are registered before elements id are. |
| _set_special_units_targets_files($self, $special_units, $document_name); |
| |
| _prepare_associated_special_units_targets($self, $associated_special_units); |
| |
| _set_root_commands_targets_node_files($self); |
| |
| _prepare_index_entries_targets($self); |
| _prepare_footnotes_targets($self); |
| |
| _set_heading_commands_targets($self); |
| |
| $self->register_output_units_lists([$output_units, |
| $special_units, $associated_special_units]); |
| |
| return ($output_units, $special_units, $associated_special_units); |
| } |
| |
| # Associate output units to the global targets, First, Last, Top, Index. |
| sub _prepare_output_units_global_targets($$$$) { |
| my ($self, $output_units, $special_units, $associated_special_units) = @_; |
| |
| $self->{'global_units_directions'}->{'First'} = $output_units->[0]; |
| $self->{'global_units_directions'}->{'Last'} = $output_units->[-1]; |
| |
| $self->{'global_units_directions'}->{'Top'} |
| = _get_top_unit($self, $output_units); |
| |
| my $global_commands; |
| my $nodes_list; |
| my $sections_list; |
| if (exists($self->{'document'})) { |
| $global_commands = $self->{'document'}->global_commands_information(); |
| $nodes_list = $self->{'document'}->nodes_list(); |
| $sections_list = $self->{'document'}->sections_list(); |
| } |
| |
| # Associate Index with the last @printindex. According to Werner Lemberg, |
| # "the most general index is normally the last one, not the first" |
| # https://lists.gnu.org/archive/html/bug-texinfo/2025-01/msg00019.html |
| # |
| # It is always the last printindex, even if it is not output (for example |
| # it is in @copying and @titlepage, which are certainly wrong constructs). |
| if (defined($global_commands) and exists($global_commands->{'printindex'})) { |
| # Here document_unit can only be a document unit, or maybe undef if there |
| # are no document unit at all |
| my ($document_unit, $root_command) |
| = _html_get_tree_root_element($self, |
| $global_commands->{'printindex'}->[-1]); |
| if (defined($document_unit)) { |
| if (defined($root_command)) { |
| my $section_relations; |
| if ($root_command->{'cmdname'} eq 'node') { |
| if (defined($nodes_list)) { |
| my $node_relations |
| = $nodes_list->[$root_command->{'extra'}->{'node_number'} -1]; |
| if (exists($node_relations->{'associated_section'})) { |
| $section_relations = $node_relations->{'associated_section'}; |
| } |
| } |
| } else { |
| $section_relations |
| = $sections_list->[$root_command->{'extra'}->{'section_number'} -1]; |
| } |
| |
| # find the first level 1 sectioning element to associate the printindex |
| # with. May not work correctly if structuring was not done |
| if ($section_relations) { |
| my $current_command = $section_relations->{'element'}; |
| while (exists($current_command->{'extra'}) |
| and defined($current_command->{'extra'}->{'section_level'}) |
| and $current_command->{'extra'}->{'section_level'} > 1 |
| and exists($section_relations->{'section_directions'}) |
| and exists($section_relations->{'section_directions'}->{'up'}) |
| and exists($section_relations->{'section_directions'}->{'up'} |
| ->{'element'}->{'associated_unit'})) { |
| $section_relations |
| = $section_relations->{'section_directions'}->{'up'}; |
| $current_command = $section_relations->{'element'}; |
| $document_unit = $current_command->{'associated_unit'}; |
| } |
| } |
| } |
| $self->{'global_units_directions'}->{'Index'} = $document_unit; |
| } |
| } |
| |
| if ($self->{'customized_global_directions'}) { |
| foreach my $direction (sort(keys(%{$self->{'customized_global_directions'}}))) { |
| my $node_texi_name |
| = $self->{'customized_global_directions'}->{$direction}; |
| if (defined($node_texi_name) |
| and not defined($self->global_direction_text($direction))) { |
| # Determine the document unit corresponding to the direction |
| # node name Texinfo code |
| |
| # Parse the customized direction node name Texinfo code |
| my $node_element; |
| my $parser = Texinfo::Parser::parser({'NO_INDEX' => 1, |
| 'NO_USER_COMMANDS' => 1,}); |
| my $tree = $parser->parse_texi_line($node_texi_name, undef, 1); |
| my $errors = $parser->errors(); |
| my $errors_count = Texinfo::Report::count_errors($errors); |
| if ($errors_count) { |
| warn "Global $direction node name parsing $errors_count error(s)\n"; |
| warn "node name: $node_texi_name\n"; |
| warn "Error messages: \n"; |
| foreach my $error_message (@$errors) { |
| warn $error_message->{'error_line'}; |
| } |
| } |
| |
| # convert to identifier and determine the node element target |
| if ($tree) { |
| my $normalized_node |
| = Texinfo::Convert::NodeNameNormalization::convert_to_node_identifier($tree); |
| if ($normalized_node ne '' and $normalized_node =~ /[^-]/) { |
| $node_element |
| = $self->converter_find_identifier_target($normalized_node); |
| } |
| } |
| if (!defined($node_element)) { |
| $self->converter_document_warn( |
| sprintf(__("could not find %s node `%s'"), |
| $direction, $node_texi_name)); |
| } else { |
| $self->{'global_units_directions'}->{$direction} |
| = $node_element->{'associated_unit'}; |
| } |
| } |
| } |
| } |
| |
| if ($self->get_conf('DEBUG')) { |
| # TODO also show the global directions added by the user |
| print STDERR "GLOBAL DIRECTIONS:\n"; |
| foreach my $global_direction (@global_directions_order) { |
| my $global_unit = $self->global_direction_unit($global_direction); |
| if (defined($global_unit)) { |
| print STDERR " $global_direction" |
| # uncomment to get the perl object name |
| # ."($global_unit)" |
| .': '. Texinfo::OutputUnits::output_unit_texi($global_unit)."\n"; |
| } |
| } |
| print STDERR "TEXT DIRECTIONS:\n"; |
| foreach my $text_direction (@text_directions_order) { |
| if ($self->global_direction_text($text_direction)) { |
| my $text_value = $self->direction_string($text_direction, 'button'); |
| if (defined($text_value)) { |
| print STDERR " $text_direction {$text_value}\n"; |
| } |
| } |
| } |
| |
| print STDERR "\n"; |
| } |
| |
| # TODO show the special units directions if DEBUG is set? |
| foreach my $units_list ($special_units, $associated_special_units) { |
| if (defined($units_list) and scalar(@$units_list)) { |
| foreach my $special_unit (@$units_list) { |
| my $special_unit_variety = $special_unit->{'special_unit_variety'}; |
| my $special_unit_direction |
| = $self->special_unit_info('direction', $special_unit_variety); |
| $self->{'global_units_directions'}->{$special_unit_direction} |
| = $special_unit; |
| } |
| } |
| } |
| } |
| |
| sub _html_set_pages_files($$$$$$$$) { |
| my ($self, $output_units, $special_units, $associated_output_units, |
| $output_file, $destination_directory, $output_filename, |
| $document_name) = @_; |
| |
| my @filenames_order; |
| my %unit_file_name_paths; |
| # associate a file to the source information leading to set the file |
| # name. Use the first element source information associated to a file. |
| # The source information can be either a tree element associated to |
| # the 'file_info_element' key, with a 'file_info_type' 'node' or |
| # 'section'... or a specific source associated to the 'file_info_name' |
| # key with 'file_info_type' 'special_file', or a source set if |
| # nothing was found, with 'file_info_type' 'stand_in_file' and a |
| # 'file_info_name'. Redirection files are added in the output() |
| # function. |
| my %files_source_info = (); |
| if (!$self->get_conf('SPLIT')) { |
| push @filenames_order, $output_filename; |
| foreach my $output_unit (@$output_units) { |
| $unit_file_name_paths{$output_unit} = $output_filename; |
| } |
| $files_source_info{$output_filename} |
| = {'file_info_type' => 'special_file', |
| 'file_info_name' => 'non_split', |
| 'file_info_path' => $output_file}; |
| } else { |
| my $identifiers_target; |
| if (exists($self->{'document'})) { |
| $identifiers_target = $self->{'document'}->labels_information(); |
| } |
| |
| # first determine the top node file name. |
| my $node_top; |
| $node_top = $identifiers_target->{'Top'} |
| if (defined($identifiers_target)); |
| |
| my $top_node_filename = $self->top_node_filename($document_name); |
| my $node_top_output_unit; |
| if (defined($node_top) and defined($top_node_filename)) { |
| $node_top_output_unit = $node_top->{'associated_unit'}; |
| die "BUG: No output unit for top node" if (!defined($node_top_output_unit)); |
| push @filenames_order, $top_node_filename; |
| $unit_file_name_paths{$node_top_output_unit} = $top_node_filename; |
| $files_source_info{$top_node_filename} |
| = {'file_info_type' => 'special_file', |
| 'file_info_name' => 'Top', |
| 'file_info_path' => undef}; |
| } |
| my $file_nr = 0; |
| my $extension = ''; |
| $extension = '.'.$self->get_conf('EXTENSION') |
| if (defined($self->get_conf('EXTENSION')) |
| and $self->get_conf('EXTENSION') ne ''); |
| |
| foreach my $output_unit (@$output_units) { |
| # For Top node. |
| next if ($node_top_output_unit and $output_unit eq $node_top_output_unit); |
| |
| my $file_output_unit = $output_unit->{'first_in_page'}; |
| if (!defined($file_output_unit)) { |
| cluck ("No first_in_page for $output_unit\n"); |
| } |
| if (not exists($unit_file_name_paths{$file_output_unit})) { |
| my $node_filename; |
| foreach my $root_command (@{$file_output_unit->{'unit_contents'}}) { |
| if (exists($root_command->{'cmdname'}) |
| and $root_command->{'cmdname'} eq 'node') { |
| # double node are not normalized, they are handled here |
| if (!exists($root_command->{'extra'}) |
| or !exists($root_command->{'extra'}->{'identifier'}) |
| or !exists($identifiers_target->{ |
| $root_command->{'extra'}->{'identifier'}})) { |
| $node_filename = 'unknown_node'; |
| $node_filename .= $extension; |
| |
| if (!exists($files_source_info{$node_filename})) { |
| push @filenames_order, $node_filename; |
| $files_source_info{$node_filename} |
| = {'file_info_type' => 'stand_in_file', |
| 'file_info_name' => 'unknown_node', |
| 'file_info_path' => undef}; |
| } |
| } else { |
| # Nodes with {'extra'}->{'is_target'} should always be in |
| # 'identifiers_target', and thus in targets. It is a bug otherwise. |
| $node_filename |
| = $self->{'targets'}->{$root_command}->{'node_filename'}; |
| if (not exists($files_source_info{$node_filename}) |
| or $files_source_info{$node_filename} |
| ->{'file_info_type'} ne 'stand_in_file') { |
| |
| push @filenames_order, $node_filename |
| unless ($files_source_info{$node_filename}); |
| |
| $files_source_info{$node_filename} |
| = {'file_info_type' => 'node', |
| 'file_info_element' => $root_command, |
| 'file_info_path' => undef}; |
| } |
| } |
| $unit_file_name_paths{$file_output_unit} = $node_filename; |
| last; |
| } |
| } |
| if (not defined($node_filename)) { |
| # use section to do the file name if there is no node |
| my $command = $file_output_unit->{'unit_section'}; |
| if (defined($command)) { |
| if ($command->{'element'}->{'cmdname'} eq 'top' |
| and !defined($node_top) and defined($top_node_filename)) { |
| $unit_file_name_paths{$file_output_unit} = $top_node_filename; |
| |
| # existing top_node_filename can happen, see |
| # html_tests.t top_file_name_and_node_name_collision |
| push @filenames_order, $top_node_filename |
| unless exists($files_source_info{$top_node_filename}); |
| |
| $files_source_info{$top_node_filename} |
| = {'file_info_type' => 'special_file', |
| 'file_info_name' => 'Top', |
| 'file_info_path' => undef}; |
| } else { |
| my $section_filename |
| = $self->{'targets'}->{$command->{'element'}} |
| ->{'section_filename'}; |
| $unit_file_name_paths{$file_output_unit} = $section_filename; |
| |
| if (not exists($files_source_info{$section_filename}) |
| or $files_source_info{$section_filename} |
| ->{'file_info_type'} ne 'stand_in_file') { |
| |
| push @filenames_order, $section_filename |
| unless (exists($files_source_info{$section_filename})); |
| |
| $files_source_info{$section_filename} |
| = {'file_info_type' => 'section', |
| 'file_info_element' => $command->{'element'}, |
| 'file_info_path' => undef}; |
| } |
| } |
| } else { |
| # when everything else has failed |
| if ($file_nr == 0 and !defined($node_top) |
| and defined($top_node_filename)) { |
| $unit_file_name_paths{$file_output_unit} = $top_node_filename; |
| unless (exists($files_source_info{$top_node_filename})) { |
| push @filenames_order, $top_node_filename; |
| $files_source_info{$top_node_filename} |
| = {'file_info_type' => 'stand_in_file', |
| 'file_info_name' => 'Top', |
| 'file_info_path' => undef}; |
| } |
| } else { |
| my $filename = $document_name . "_$file_nr"; |
| $filename .= $extension; |
| $unit_file_name_paths{$file_output_unit} = $filename; |
| |
| unless (exists($files_source_info{$filename})) { |
| push @filenames_order, $filename; |
| $files_source_info{$filename} |
| = {'file_info_type' => 'stand_in_file', |
| 'file_info_name' => 'unknown', |
| 'file_info_path' => undef}; |
| } |
| } |
| $file_nr++; |
| } |
| } |
| } |
| if ($output_unit ne $file_output_unit) { |
| $unit_file_name_paths{$output_unit} |
| = $unit_file_name_paths{$file_output_unit} |
| } |
| } |
| } |
| |
| my $debug = $self->get_conf('DEBUG'); |
| foreach my $output_unit (@$output_units) { |
| my $filename = $unit_file_name_paths{$output_unit}; |
| my $file_source_info = $files_source_info{$filename}; |
| # check |
| if (!defined($file_source_info)) { |
| print STDERR "BUG: no files_source_info: $filename\n"; |
| } |
| my $filepath = $file_source_info->{'file_info_path'}; |
| if (defined($self->{'file_id_setting'}->{'unit_file_name'})) { |
| # NOTE the information that it is associated with @top or @node Top |
| # may be determined with $self->unit_is_top_output_unit($output_unit); |
| my ($user_filename, $user_filepath) |
| = &{$self->{'file_id_setting'}->{'unit_file_name'}}( |
| $self, $output_unit, $filename, $filepath); |
| if (defined($user_filename)) { |
| my $user_file_source_info; |
| if (exists($files_source_info{$user_filename})) { |
| $user_file_source_info = $files_source_info{$user_filename}; |
| my $previous_filepath = $user_file_source_info->{'file_info_path'}; |
| # It is likely that setting different paths for the same file is |
| # not intended, so we warn. |
| if (defined($user_filepath) and defined($previous_filepath) |
| and $user_filepath ne $previous_filepath) { |
| $self->converter_document_warn( |
| sprintf(__("resetting %s file path %s to %s"), |
| $user_filename, $previous_filepath, $user_filepath)); |
| } elsif (defined($user_filepath) and !defined($previous_filepath)) { |
| $self->converter_document_warn( |
| sprintf(__("resetting %s file path from a relative path to %s"), |
| $user_filename, $user_filepath)); |
| } elsif (!defined($user_filepath) and defined($previous_filepath)) { |
| $self->converter_document_warn( |
| sprintf(__("resetting %s file path from %s to a relative path"), |
| $user_filename, $previous_filepath)); |
| } |
| } |
| $filename = $user_filename; |
| push @filenames_order, $filename |
| unless (defined($user_file_source_info)); |
| $files_source_info{$filename} = {'file_info_type' => 'special_file', |
| 'file_info_name' => 'user_defined', |
| 'file_info_path' => $user_filepath}; |
| } |
| } |
| $self->set_output_unit_file($output_unit, $filename); |
| my $output_unit_filename = $output_unit->{'unit_filename'}; |
| $self->{'file_counters'}->{$output_unit_filename} = 0 |
| if (!exists($self->{'file_counters'}->{$output_unit_filename})); |
| $self->{'file_counters'}->{$output_unit_filename}++; |
| print STDERR 'Page ' |
| # uncomment for perl object name |
| #."$output_unit " |
| .Texinfo::OutputUnits::output_unit_texi($output_unit) |
| .": $output_unit_filename($self->{'file_counters'}->{$output_unit_filename})\n" |
| if ($debug); |
| } |
| |
| if (defined($special_units)) { |
| foreach my $special_unit (@$special_units) { |
| my $unit_command = $special_unit->{'unit_command'}; |
| my $filename |
| = $self->{'targets'}->{$unit_command}->{'special_unit_filename'}; |
| # Associate the special elements that have no page with the main page. |
| # This may only happen if not split. |
| if (!defined($filename) |
| and defined($output_units->[0]->{'unit_filename'})) { |
| $filename = $output_units->[0]->{'unit_filename'}; |
| } |
| if (defined($filename)) { |
| push @filenames_order, $filename |
| unless exists($files_source_info{$filename}); |
| $self->set_output_unit_file($special_unit, $filename); |
| $self->{'file_counters'}->{$filename} = 0 |
| if (!exists($self->{'file_counters'}->{$filename})); |
| $self->{'file_counters'}->{$filename}++; |
| print STDERR 'Special page' |
| # uncomment for perl object name |
| #." $special_unit" |
| .": $filename($self->{'file_counters'}->{$filename})\n" |
| if ($debug); |
| my $file_source_info = {'file_info_element' => $unit_command, |
| 'file_info_type' => 'special_unit', |
| 'file_info_path' => undef}; |
| $files_source_info{$filename} = $file_source_info |
| unless(exists($files_source_info{$filename}) |
| and $files_source_info{$filename}->{'file_info_type'} |
| ne 'stand_in_file'); |
| } |
| } |
| } |
| |
| foreach my $filename (@filenames_order) { |
| $self->set_file_path($filename, $destination_directory, |
| $files_source_info{$filename}->{'file_info_path'}); |
| } |
| |
| # to be able to associate to the output unit file the associated |
| # output units will be output into, this is done after document output |
| # units got files. |
| # In practice only used for contents and shortcontents. |
| if (defined($associated_output_units) |
| and scalar(@$associated_output_units)) { |
| foreach my $special_unit (@$associated_output_units) { |
| my $associated_output_unit = $special_unit->{'associated_document_unit'}; |
| my $unit_command = $special_unit->{'unit_command'}; |
| my $filename; |
| |
| my $command_target = $self->{'targets'}->{$unit_command}; |
| # set by the user |
| if (defined($command_target->{'special_unit_filename'})) { |
| $filename = $command_target->{'special_unit_filename'}; |
| } else { |
| $filename = $associated_output_unit->{'unit_filename'} |
| if ($associated_output_unit); |
| $command_target->{'special_unit_filename'} = $filename; |
| } |
| |
| # set here the file name, but do not associate a counter as it is already |
| # set for the output unit the special output unit is in. |
| $self->set_output_unit_file($special_unit, $filename) |
| if (defined($filename)); |
| } |
| } |
| |
| return \%files_source_info; |
| } |
| |
| sub _prepare_special_units_directions($$) { |
| my ($self, $special_units) = @_; |
| |
| return unless(defined($special_units)); |
| |
| foreach my $special_unit (@$special_units) { |
| $special_unit->{'directions'}->{'This'} = $special_unit; |
| } |
| } |
| |
| sub _prepare_units_directions_files($$$$$$$$) { |
| my ($self, $output_units, $special_units, $associated_special_units, |
| $output_file, $destination_directory, $output_filename, |
| $document_name) = @_; |
| |
| my $identifiers_target; |
| my $nodes_list; |
| if (exists($self->{'document'})) { |
| $identifiers_target = $self->{'document'}->labels_information(); |
| $nodes_list = $self->{'document'}->nodes_list(); |
| } |
| |
| _prepare_output_units_global_targets($self, $output_units, $special_units, |
| $associated_special_units); |
| |
| Texinfo::OutputUnits::split_pages($output_units, $nodes_list, |
| $self->get_conf('SPLIT')); |
| |
| # reset even if $output_file eq '', as 'file_counters' is accessed |
| # below, so it needs to be empty in the case of $output_file eq ''. |
| $self->initialize_output_units_files(); |
| |
| # determine file names associated with the different pages, and setup |
| # the counters for special element pages. |
| my $files_source_info; |
| if ($output_file ne '') { |
| $files_source_info = |
| _html_set_pages_files($self, $output_units, $special_units, |
| $associated_special_units, $output_file, |
| $destination_directory, $output_filename, $document_name); |
| } |
| |
| # do output units directions. |
| Texinfo::OutputUnits::units_directions($identifiers_target, $nodes_list, |
| $output_units, |
| $self->get_conf('DEBUG')); |
| |
| _prepare_special_units_directions($self, $special_units); |
| |
| # do output units directions related to files. |
| # Here such that PrevFile and NextFile can be set. |
| Texinfo::OutputUnits::units_file_directions($output_units); |
| |
| # elements_in_file_count is only set in HTML, not in |
| # Texinfo::Convert::Converter |
| $self->{'elements_in_file_count'} = {}; |
| # condition could also be based on $output_file ne '' |
| if (exists($self->{'file_counters'})) { |
| # 'file_counters' is dynamic, decreased when the element is encountered |
| # 'elements_in_file_count' is not modified afterwards |
| foreach my $filename (keys(%{$self->{'file_counters'}})) { |
| $self->{'elements_in_file_count'}->{$filename} |
| = $self->{'file_counters'}->{$filename}; |
| } |
| } |
| |
| #if (1 or $self->get_conf('DEBUG') >= 30) { |
| # if ($self->{'document'}) { |
| # my $tree = $self->{'document'}->tree(); |
| # my $use_filename = 0; |
| # if ($self->get_conf('TEST')) { |
| # $use_filename = 1; |
| # } |
| # my $output_units_output |
| # = Texinfo::OutputUnits::print_output_units_tree_details($output_units, |
| # $tree, $use_filename); |
| # } |
| #} |
| |
| return $files_source_info; |
| } |
| |
| sub convert_output_unit($$;$) { |
| my ($self, $output_unit, |
| # only used for debug |
| $explanation) = @_; |
| |
| my $debug = $self->get_conf('DEBUG'); |
| |
| my $unit_type_name = $output_unit->{'unit_type'}; |
| |
| if (exists($self->{'output_units_conversion'}->{$unit_type_name}) |
| and !defined($self->{'output_units_conversion'}->{$unit_type_name})) { |
| if ($debug) { |
| print STDERR "IGNORED OU $unit_type_name\n"; |
| } |
| return ''; |
| } |
| |
| if ($debug) { |
| print STDERR "UNIT($explanation) -> ou: $unit_type_name '" |
| .Texinfo::OutputUnits::output_unit_texi($output_unit)."'\n"; |
| } |
| |
| $self->{'current_output_unit'} = $output_unit; |
| |
| my $content_formatted = ''; |
| if (exists($output_unit->{'unit_contents'})) { |
| my $content_idx = 0; |
| foreach my $content (@{$output_unit->{'unit_contents'}}) { |
| $content_formatted |
| .= _convert($self, $content, "$unit_type_name c[$content_idx]"); |
| $content_idx++; |
| } |
| } |
| my $result = ''; |
| if (defined($self->{'output_units_conversion'}->{$unit_type_name})) { |
| $result |
| .= &{$self->{'output_units_conversion'}->{$unit_type_name}} ($self, |
| $unit_type_name, |
| $output_unit, |
| $content_formatted); |
| } elsif (defined($content_formatted)) { |
| $result .= $content_formatted; |
| } |
| |
| delete $self->{'current_output_unit'}; |
| |
| print STDERR "DOUNIT ($unit_type_name) => `$result'\n" if $debug; |
| |
| return $result; |
| } |
| |
| sub _run_stage_handlers($$$$) { |
| my ($converter, $stage_handlers, $document, $stage) = @_; |
| |
| return 0 if (!defined($stage_handlers->{$stage})); |
| |
| my $handler_idx = 1; |
| foreach my $handler_and_priority (@{$stage_handlers->{$stage}}) { |
| my ($handler, $priority) = @$handler_and_priority; |
| if ($converter->get_conf('DEBUG')) { |
| print STDERR "RUN handler $handler_idx: stage $stage, priority $priority\n"; |
| } |
| my $status = &{$handler}($converter, $document, $stage); |
| if (!defined($status) or ref($status) ne '' or $status !~ /^\d+$/) { |
| $converter->converter_document_error( |
| sprintf(__("handler %d of stage %s priority %s: non-numeric status"), |
| $handler_idx, $stage, $priority)); |
| $status = $converter->get_conf('HANDLER_FATAL_ERROR_LEVEL') +1; |
| } |
| if ($status != 0) { |
| if ($status < 0) { |
| $converter->converter_document_error( |
| sprintf(__("handler %d of stage %s priority %s failed"), |
| $handler_idx, $stage, $priority)); |
| } else { |
| # the handler is supposed to have output an error message |
| # already if $status > 0 |
| if ($converter->get_conf('VERBOSE') or $converter->get_conf('DEBUG')) { |
| print STDERR "FAIL handler $handler_idx: stage $stage, " |
| ."priority $priority, status $status\n"; |
| } |
| } |
| return $status; |
| } |
| $handler_idx++; |
| } |
| return 0; |
| } |
| |
| sub _prepare_converted_output_info($$$$) { |
| my ($self, $output_file, $output_filename, $output_units) = @_; |
| |
| my $stage_handlers = $self->{'stage_handlers'}; |
| |
| my $structure_status = _run_stage_handlers($self, $stage_handlers, |
| $self->{'document'}, 'structure'); |
| my $handler_fatal_error_level = $self->get_conf('HANDLER_FATAL_ERROR_LEVEL'); |
| |
| unless ($structure_status < $handler_fatal_error_level |
| and $structure_status > -$handler_fatal_error_level) { |
| return 0; |
| } |
| |
| my $global_commands; |
| if (exists($self->{'document'})) { |
| $global_commands = $self->{'document'}->global_commands_information(); |
| } |
| |
| my $default_bcp47_locale = $self->current_bcp47_locale(); |
| |
| # This also sets the language related options to their pre-conversion |
| # values (which is not really needed here). |
| $self->set_converter_preamble_language_commands(); |
| |
| my $preamble_bcp47_locale = $self->current_bcp47_locale(); |
| |
| if ($default_bcp47_locale ne $preamble_bcp47_locale) { |
| _translate_names($self); |
| } |
| |
| # prepare title. fulltitle uses more possibility than simpletitle for |
| # title, including @-commands found in @titlepage only. Therefore |
| # simpletitle is more in line with what makeinfo in C did. |
| |
| _prepare_simpletitle($self); |
| |
| my $fulltitle_tree; |
| if (defined($global_commands)) { |
| foreach my $fulltitle_command ('settitle', 'title', |
| 'shorttitlepage') { |
| if (exists($global_commands->{$fulltitle_command})) { |
| my $command = $global_commands->{$fulltitle_command}; |
| next if (!exists($command->{'contents'}->[0]->{'contents'})); |
| $fulltitle_tree = $command->{'contents'}->[0]; |
| last; |
| } |
| } |
| if (!defined($fulltitle_tree) and exists($global_commands->{'top'})) { |
| # arguments_line type element |
| my $arguments_line = $global_commands->{'top'}->{'contents'}->[0]; |
| my $line_arg = $arguments_line->{'contents'}->[0]; |
| if (exists($line_arg->{'contents'})) { |
| $fulltitle_tree = $line_arg; |
| } |
| } |
| if (!defined($fulltitle_tree) and exists($global_commands->{'titlefont'}) |
| and exists($global_commands->{'titlefont'}->[0]->{'contents'}) |
| and exists($global_commands->{'titlefont'}->[0]->{'contents'}->[0] |
| ->{'contents'})) { |
| $fulltitle_tree = $global_commands->{'titlefont'}->[0]; |
| } |
| } |
| |
| my $html_title_string; |
| my $title_tree; |
| if (defined($fulltitle_tree)) { |
| $title_tree = $fulltitle_tree; |
| $html_title_string |
| = $self->convert_tree_new_formatting_context($title_tree, |
| 'title_string', |
| $CTXF_string); |
| if ($html_title_string !~ /\S/) { |
| $html_title_string = undef; |
| } |
| } |
| if (!defined($html_title_string)) { |
| my $default_title = $self->cdt('Untitled Document'); |
| $title_tree = $default_title; |
| $self->{'converter_info'}->{'title_tree'} = $title_tree; |
| $self->{'converter_info'}->{'title_string'} |
| = $self->convert_tree_new_formatting_context($title_tree, |
| 'title_string', |
| $CTXF_string); |
| my $input_file_name; |
| if (exists($self->{'document'})) { |
| my $document_info = $self->{'document'}->global_information(); |
| if (defined($document_info)) { |
| $input_file_name = $document_info->{'input_file_name'}; |
| } |
| } |
| |
| if (defined($input_file_name)) { |
| $self->converter_line_warn(__( |
| "must specify a title with a title command or \@top"), |
| {'file_name' => $input_file_name}); |
| } else { |
| $self->converter_document_warn(__( |
| "must specify a title with a title command or \@top")); |
| } |
| } else { |
| $self->{'converter_info'}->{'title_tree'} = $title_tree; |
| $self->{'converter_info'}->{'title_string'} = $html_title_string; |
| } |
| |
| # copying comment |
| if (defined($global_commands) and exists($global_commands->{'copying'})) { |
| my $copying_comment = Texinfo::Convert::Text::convert_to_text( |
| Texinfo::TreeElement::new( |
| {'contents' => $global_commands->{'copying'}->{'contents'}}), |
| $self->{'convert_text_options'}); |
| if ($copying_comment ne '') { |
| $self->{'converter_info'}->{'copying_comment'} |
| = &{$self->formatting_function('format_comment')}($self, $copying_comment); |
| } |
| } |
| |
| # documentdescription |
| if (defined($self->get_conf('documentdescription'))) { |
| $self->{'converter_info'}->{'documentdescription_string'} |
| = $self->get_conf('documentdescription'); |
| } elsif (defined($global_commands) |
| and exists($global_commands->{'documentdescription'})) { |
| my $tmp = Texinfo::TreeElement::new({'contents' |
| => $global_commands->{'documentdescription'}->{'contents'}}); |
| |
| my $documentdescription_string |
| = $self->convert_tree_new_formatting_context($tmp, |
| 'documentdescription', $CTXF_string); |
| |
| chomp($documentdescription_string); |
| $self->{'converter_info'}->{'documentdescription_string'} |
| = $documentdescription_string; |
| } |
| |
| # documentinfo metadata |
| if (defined($global_commands) |
| and exists($global_commands->{'documentinfo'})) { |
| my $documentinfo = $global_commands->{'documentinfo'}; |
| if (exists($documentinfo->{'contents'})) { |
| my $metadata_in_documentinfo = ''; |
| foreach my $element (@{$documentinfo->{'contents'}}) { |
| if ((exists($element->{'type'}) |
| and $element->{'type'} eq 'arguments_line' |
| or (exists($element->{'cmdname'}) |
| and (exists( |
| $Texinfo::Commands::metadata_commands{$element->{'cmdname'}}) |
| # If EPUB is expanded, @html metadata is used for the EPUB specific |
| # metadata, not for each converted HTML file metadata included in the EPUB |
| # container. This allows to have EPUB metadata even without a specific |
| # @epub raw format command. |
| or ($element->{'cmdname'} eq 'html' |
| and $self->is_format_expanded('epub')))))) { |
| next; |
| } |
| $metadata_in_documentinfo |
| .= $self->convert_tree_new_formatting_context($element, |
| 'documentinfo metadata'); |
| } |
| if ($metadata_in_documentinfo =~ /\S/) { |
| $self->{'converter_info'}->{'documentinfo_metadata'} |
| = $metadata_in_documentinfo; |
| } |
| } |
| } |
| |
| if ($self->get_conf('DATE_IN_HEADER')) { |
| my $today |
| = $self->convert_tree_new_formatting_context( |
| Texinfo::TreeElement::new({'cmdname' => 'today'}), |
| 'DATE_IN_HEADER'); |
| my $date = |
| $self->close_html_lone_element( |
| "<meta name=\"date\" content=\"$today\"")."\n"; |
| |
| $self->{'converter_info'}->{'date_in_header'} = $date; |
| } |
| |
| my $init_status = _run_stage_handlers($self, $stage_handlers, |
| $self->{'document'}, 'init'); |
| unless ($init_status < $handler_fatal_error_level |
| and $init_status > -$handler_fatal_error_level) { |
| return 0; |
| } |
| |
| _prepare_title_titlepage($self, $output_file, $output_filename, |
| $output_units); |
| |
| if ($default_bcp47_locale ne $preamble_bcp47_locale) { |
| $self->reset_lang_translation_from_customization( |
| $self->get_conf('documentlanguage'), |
| $self->get_conf('documentscript')); |
| _translate_names($self); |
| } |
| |
| # reset in case the user changed customization variables in handlers |
| $self->{'convert_text_options'} |
| = Texinfo::Convert::Text::copy_options_for_convert_text($self); |
| |
| return 1; |
| } |
| |
| # units or root conversion |
| sub _html_convert_output($$$$$$$$) { |
| my ($self, $output_file, $destination_directory, $output_filename, |
| $document_name, $document, $output_units, $special_units) = @_; |
| |
| my ($encoded_destination_directory, $dir_encoding) |
| = $self->encoded_output_file_name($destination_directory); |
| my $succeeded |
| = $self->create_destination_directory($encoded_destination_directory, |
| $destination_directory); |
| if (!$succeeded) { |
| return undef; |
| } |
| |
| my $debug = $self->get_conf('DEBUG'); |
| my $text_output = ''; |
| if ($output_file eq '') { |
| $self->{'current_filename'} = $output_filename; |
| my $body = ''; |
| my $unit_nr = 0; |
| # NOTE there is no rule before the footnotes special element in |
| # case of separate footnotes in this setting. |
| foreach my $output_unit (@$output_units, @$special_units) { |
| print STDERR "\nUNIT NO-PAGE $unit_nr\n" if ($debug); |
| my $output_unit_text |
| = $self->convert_output_unit($output_unit, |
| "no-page output unit $unit_nr"); |
| $body .= $output_unit_text; |
| $unit_nr++; |
| } |
| |
| # do end file first, in case it needs some CSS |
| my $file_end = &{$self->formatting_function('format_end_file')}($self, |
| $output_filename, undef); |
| my $file_beginning |
| = &{$self->formatting_function('format_begin_file')}($self, |
| $output_filename, undef); |
| $text_output .= $file_beginning; |
| $text_output .= $body; |
| $text_output .= $file_end; |
| } else { |
| # output with pages |
| print STDERR "DO Units with filenames\n" if ($debug); |
| my %files; |
| |
| my $unit_nr = -1; |
| # Now do the output, converting each output units and special output units |
| # in turn |
| $special_units = [] if (!defined($special_units)); |
| foreach my $output_unit (@$output_units, @$special_units) { |
| $unit_nr++; |
| |
| my $output_unit_filename = $output_unit->{'unit_filename'}; |
| $self->{'current_filename'} = $output_unit_filename; |
| |
| # convert body before header in case this affects the header |
| # and, for special output unit, to avoid outputting anything if empty. |
| my $body; |
| if ($output_unit->{'unit_type'} eq 'special_unit') { |
| print STDERR "\nUNIT SPECIAL " |
| ."$output_unit->{'special_unit_variety'} $unit_nr\n" |
| if ($debug); |
| $body = $self->convert_output_unit($output_unit, |
| "output s-unit $unit_nr"); |
| if ($body eq '') { |
| $body = undef; |
| } |
| } else { |
| print STDERR "\nUNIT $unit_nr\n" if ($debug); |
| $body = $self->convert_output_unit($output_unit, |
| "output unit $unit_nr"); |
| } |
| |
| $self->{'file_counters'}->{$output_unit_filename}--; |
| |
| # register the output but do not print anything. Printing |
| # only when file_counters reach 0, to be sure that all the |
| # elements have been converted before headers are done. |
| if (defined($body)) { |
| if (!exists($files{$output_unit_filename})) { |
| $files{$output_unit_filename} = {'first_unit' => $output_unit, |
| 'body' => ''}; |
| } |
| $files{$output_unit_filename}->{'body'} .= $body; |
| } else { |
| next if (!exists($files{$output_unit_filename}) |
| or $files{$output_unit_filename}->{'body'} eq ''); |
| } |
| |
| if ($self->{'file_counters'}->{$output_unit_filename} == 0) { |
| my $out_filepath = $self->{'out_filepaths'}->{$output_unit_filename}; |
| my $file_output_unit = $files{$output_unit_filename}->{'first_unit'}; |
| my ($encoded_out_filepath, $path_encoding) |
| = $self->encoded_output_file_name($out_filepath); |
| # the third return information, set if the file has already been used |
| # in this files_information is not checked as this cannot happen. |
| my ($file_fh, $error_message) |
| = Texinfo::Convert::Utils::output_files_open_out( |
| $self->output_files_information(), |
| $encoded_out_filepath, undef, |
| $self->get_conf('OUTPUT_ENCODING_NAME')); |
| if (!defined($file_fh)) { |
| $self->converter_document_error( |
| sprintf(__("could not open %s for writing: %s"), |
| $out_filepath, $error_message)); |
| delete $self->{'current_filename'}; |
| return undef; |
| } |
| # do end file first in case it requires some CSS |
| my $end_file = &{$self->formatting_function('format_end_file')}($self, |
| $output_unit_filename, |
| $output_unit); |
| print $file_fh "".&{$self->formatting_function('format_begin_file')}( |
| $self, $output_unit_filename, $file_output_unit); |
| print $file_fh "".$files{$output_unit_filename}->{'body'}; |
| # end file |
| print $file_fh "". $end_file; |
| |
| # Do not close STDOUT now such that the file descriptor is not reused |
| # by open, which uses the lowest-numbered file descriptor not open, |
| # for another filehandle. Closing STDOUT is handled by the caller. |
| if ($out_filepath ne '-') { |
| Texinfo::Convert::Utils::output_files_register_closed( |
| $self->output_files_information(), $encoded_out_filepath); |
| if (!close($file_fh)) { |
| $self->converter_document_error( |
| sprintf(__("error on closing %s: %s"), |
| $out_filepath, $!)); |
| delete $self->{'current_filename'}; |
| return undef; |
| } |
| } |
| } |
| } |
| } |
| delete $self->{'current_filename'}; |
| return $text_output; |
| } |
| |
| sub _prepare_node_redirection_page($$$) { |
| my ($self, $target_element, $redirection_filename) = @_; |
| |
| $self->{'current_filename'} = $redirection_filename; |
| |
| my $redirection_page |
| = &{$self->formatting_function('format_node_redirection_page')}($self, |
| $target_element, $redirection_filename); |
| delete $self->{'current_filename'}; |
| |
| return $redirection_page; |
| } |
| |
| sub _node_redirections($$$$) { |
| my ($self, $output_file, $destination_directory, $files_source_info) = @_; |
| |
| my $labels_list; |
| if (exists($self->{'document'})) { |
| $labels_list = $self->{'document'}->labels_list(); |
| } |
| |
| my $redirection_files_done = 0; |
| # do node redirection pages |
| delete $self->{'current_filename'}; |
| if ($self->get_conf('NODE_FILES') |
| and defined($labels_list) and $output_file ne '') { |
| |
| my $add_translit_redirection = 0; |
| |
| my $added_translit_extension; |
| if ($self->get_conf('TRANSLITERATE_FILE_NAMES')) { |
| $add_translit_redirection = 1; |
| $added_translit_extension = ''; |
| $added_translit_extension = '.'.$self->get_conf('EXTENSION') |
| if (defined($self->get_conf('EXTENSION')) |
| and $self->get_conf('EXTENSION') ne ''); |
| } |
| |
| my %redirection_filenames; |
| foreach my $target_element (@$labels_list) { |
| next if (not exists($target_element->{'extra'}) |
| or not $target_element->{'extra'}->{'is_target'}); |
| my $label_element = Texinfo::Common::get_label_element($target_element); |
| # filename may not be defined in case of an @anchor or similar in |
| # @titlepage, and @titlepage is not used. |
| my $filename = $self->command_filename($target_element); |
| next if (!defined($filename)); |
| |
| my $node_filename; |
| my $normalized = $target_element->{'extra'}->{'identifier'}; |
| # NOTE 'node_filename' is not used for Top, TOP_NODE_FILE_TARGET |
| # is. The other manual must use the same convention to get it |
| # right. We do not do 'node_filename' as a redirection file |
| # either. |
| if ($normalized eq 'Top' |
| and defined($self->get_conf('TOP_NODE_FILE_TARGET'))) { |
| $node_filename = $self->get_conf('TOP_NODE_FILE_TARGET'); |
| } else { |
| my ($target_filebase, $external_file_extension, $id) |
| = _standard_label_id_file($self, $normalized, $label_element, |
| $self->get_conf('EXTERNAL_CROSSREF_EXTENSION'), |
| $defaults{'EXTENSION'}); |
| $node_filename = $target_filebase.$external_file_extension; |
| } |
| |
| my @redirection_files; |
| my $node_redirection_filename |
| = $self->register_normalize_case_filename($node_filename); |
| if ($node_filename ne $filename) { |
| # first condition finds conflict with tree elements |
| if ($self->count_elements_in_filename('total', |
| $node_redirection_filename) |
| or exists($redirection_filenames{$node_redirection_filename})) { |
| $self->converter_line_warn( |
| sprintf(__("\@%s `%s' file %s for redirection exists"), |
| $target_element->{'cmdname'}, |
| Texinfo::Convert::Texinfo::convert_contents_to_texinfo( |
| $label_element), |
| $node_redirection_filename), |
| $target_element->{'source_info'}); |
| my $file_source = $files_source_info->{$node_redirection_filename}; |
| my $file_info_type = $file_source->{'file_info_type'}; |
| if ($file_info_type eq 'special_file' |
| or $file_info_type eq 'stand_in_file') { |
| my $name = $file_source->{'file_info_name'}; |
| if ($name eq 'non_split') { |
| # This cannot actually happen, as the @anchor/@node/@float |
| # with potentially conflicting name will also be in the |
| # non-split output document and therefore does not need |
| # a redirection. |
| $self->converter_document_warn( |
| __("conflict with whole document file"), 1); |
| } elsif ($name eq 'Top') { |
| $self->converter_document_warn( |
| __("conflict with Top file"), 1); |
| } elsif ($name eq 'user_defined') { |
| $self->converter_document_warn( |
| __("conflict with user-defined file"), 1); |
| } elsif ($name eq 'unknown_node') { |
| $self->converter_document_warn( |
| __("conflict with unknown node file"), 1); |
| } elsif ($name eq 'unknown') { |
| $self->converter_document_warn( |
| __("conflict with file without known source"), 1); |
| } |
| } elsif ($file_info_type eq 'node') { |
| my $conflicting_node = $file_source->{'file_info_element'}; |
| my $label_element |
| = Texinfo::Common::get_label_element($conflicting_node); |
| $self->converter_line_warn( |
| sprintf(__p('conflict of redirection file with file based on node name', |
| "conflict with \@%s `%s' file"), |
| $conflicting_node->{'cmdname'}, |
| Texinfo::Convert::Texinfo::convert_contents_to_texinfo( |
| $label_element) |
| ), |
| $conflicting_node->{'source_info'}, 1); |
| } elsif ($file_info_type eq 'redirection') { |
| my $conflicting_node = $file_source->{'file_info_element'}; |
| my $conflicting_label_element |
| = $file_source->{'file_info_label_element'}; |
| $self->converter_line_warn( |
| sprintf(__("conflict with \@%s `%s' redirection file"), |
| $conflicting_node->{'cmdname'}, |
| Texinfo::Convert::Texinfo::convert_contents_to_texinfo( |
| $conflicting_label_element) |
| ), |
| $conflicting_node->{'source_info'}, 1); |
| } elsif ($file_info_type eq 'section') { |
| my $conflicting_section = $file_source->{'file_info_element'}; |
| # arguments_line type element |
| my $arguments_line = $conflicting_section->{'contents'}->[0]; |
| my $line_arg = $arguments_line->{'contents'}->[0]; |
| $self->converter_line_warn( |
| sprintf(__p('conflict of redirection file with file based on section name', |
| "conflict with \@%s `%s' file"), |
| $conflicting_section->{'cmdname'}, |
| Texinfo::Convert::Texinfo::convert_contents_to_texinfo( |
| $line_arg), |
| ), |
| $conflicting_section->{'source_info'}, 1); |
| } elsif ($file_info_type eq 'special_unit') { |
| my $unit_command = $file_source->{'file_info_element'}; |
| my $special_unit = $unit_command->{'associated_unit'}; |
| my $output_unit_variety |
| = $special_unit->{'special_unit_variety'}; |
| $self->converter_document_warn( |
| sprintf(__("conflict with %s special element"), |
| $output_unit_variety), 1); |
| } |
| } else { |
| push @redirection_files, $node_redirection_filename; |
| } |
| } |
| |
| if ($add_translit_redirection and $normalized ne 'Top') { |
| # based on Texinfo::Convert::Converter node_information_filename |
| |
| my $translit_filename |
| = Texinfo::Convert::NodeNameNormalization::normalize_transliterate_texinfo( |
| Texinfo::TreeElement::new( |
| {'contents' => $label_element->{'contents'}})); |
| |
| $translit_filename = $self->_id_to_filename($translit_filename); |
| $translit_filename = $translit_filename.$added_translit_extension; |
| |
| if ($translit_filename ne $node_redirection_filename |
| and $translit_filename ne $filename) { |
| my $translit_redirection_filename |
| = $self->register_normalize_case_filename($translit_filename); |
| |
| if (!$self->count_elements_in_filename('total', |
| $translit_redirection_filename) |
| and not exists( |
| $redirection_filenames{$translit_redirection_filename})) { |
| push @redirection_files, $translit_redirection_filename; |
| } |
| } |
| } |
| |
| if (defined($self->{'file_id_setting'}->{'redirection_file_names'})) { |
| my $reference_redirection_files = [@redirection_files]; |
| # NOTE there are no checks on returned redirection files not |
| # replacing other files, it is up to the user to do what they |
| # should. |
| @redirection_files |
| = &{$self->{'file_id_setting'}->{'redirection_file_names'}}($self, |
| $target_element, $filename, |
| $node_redirection_filename, |
| $reference_redirection_files); |
| } |
| |
| foreach my $redirection_filename (@redirection_files) { |
| $redirection_filenames{$redirection_filename} = $target_element; |
| $files_source_info->{$redirection_filename} |
| = {'file_info_type' => 'redirection', |
| 'file_info_element' => $target_element, |
| 'file_info_path' => undef, |
| 'file_info_label_element' => $label_element}; |
| |
| my $redirection_page |
| = _prepare_node_redirection_page($self, $target_element, |
| $redirection_filename); |
| |
| my $out_filepath; |
| if ($destination_directory ne '') { |
| $out_filepath = join('/', ($destination_directory, |
| $redirection_filename)); |
| } else { |
| $out_filepath = $redirection_filename; |
| } |
| my ($encoded_out_filepath, $path_encoding) |
| = $self->encoded_output_file_name($out_filepath); |
| # the third return information, set if the file has already been used |
| # in this files_information is not checked as this cannot happen. |
| my ($file_fh, $error_message) |
| = Texinfo::Convert::Utils::output_files_open_out( |
| $self->output_files_information(), |
| $encoded_out_filepath, undef, |
| $self->get_conf('OUTPUT_ENCODING_NAME')); |
| if (!defined($file_fh)) { |
| $self->converter_document_error(sprintf(__( |
| "could not open %s for writing: %s"), |
| $out_filepath, $error_message)); |
| } else { |
| print $file_fh $redirection_page; |
| Texinfo::Convert::Utils::output_files_register_closed( |
| $self->output_files_information(), $encoded_out_filepath); |
| if (!close($file_fh)) { |
| $self->converter_document_error(sprintf(__( |
| "error on closing redirection node file %s: %s"), |
| $out_filepath, $!)); |
| $self->conversion_finalization(); |
| return undef; |
| } |
| } |
| $redirection_files_done++; |
| # NOTE failure to open a file does not stop the processing |
| } |
| } |
| } |
| return $redirection_files_done; |
| } |
| |
| sub _setup_output($) { |
| my $self = shift; |
| |
| # Should not actually be needed, as it is already deleted after conversion |
| # and each time it is set out of the conversion. |
| delete $self->{'current_filename'}; |
| |
| # no splitting when writing to the null device or to stdout or returning |
| # a string |
| if (defined($self->get_conf('OUTFILE')) |
| and ($Texinfo::Common::null_device_file{$self->get_conf('OUTFILE')} |
| or $self->get_conf('OUTFILE') eq '-' |
| or $self->get_conf('OUTFILE') eq '')) { |
| $self->force_conf('SPLIT', ''); |
| $self->force_conf('MONOLITHIC', 1); |
| } |
| if ($self->get_conf('SPLIT')) { |
| $self->set_conf('NODE_FILES', 1); |
| } |
| $self->set_conf('EXTERNAL_CROSSREF_SPLIT', $self->get_conf('SPLIT')); |
| |
| my $handler_fatal_error_level = $self->get_conf('HANDLER_FATAL_ERROR_LEVEL'); |
| if (!defined($handler_fatal_error_level)) { |
| $handler_fatal_error_level = |
| $Texinfo::Options::converter_customization_options{ |
| 'HANDLER_FATAL_ERROR_LEVEL'}; |
| $self->force_conf('HANDLER_FATAL_ERROR_LEVEL', |
| $handler_fatal_error_level); |
| } |
| |
| if ($self->get_conf('HTML_MATH') |
| and $self->get_conf('HTML_MATH') eq 'mathjax') { |
| # See https://www.gnu.org/licenses/javascript-labels.html |
| # |
| # The link to the source for mathjax does not strictly follow the advice |
| # there: instead we link to instructions for obtaining the full source in |
| # its preferred form of modification. |
| |
| my $mathjax_script = $self->get_conf('MATHJAX_SCRIPT'); |
| if (! defined($mathjax_script)) { |
| $mathjax_script = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js'; |
| $self->set_conf('MATHJAX_SCRIPT', $mathjax_script); |
| } |
| |
| my $mathjax_source = $self->get_conf('MATHJAX_SOURCE'); |
| if (! defined($mathjax_source)) { |
| $mathjax_source = 'http://docs.mathjax.org/en/latest/web/hosting.html#getting-mathjax-via-git'; |
| $self->set_conf('MATHJAX_SOURCE', $mathjax_source); |
| } |
| } |
| |
| my $setup_status = _run_stage_handlers($self, $self->{'stage_handlers'}, |
| $self->{'document'}, 'setup'); |
| |
| if ($setup_status < $handler_fatal_error_level |
| and $setup_status > -$handler_fatal_error_level) { |
| } else { |
| return undef; |
| } |
| |
| # the settable commands configuration has potentially been modified for |
| # this output file especially in setup handler. Update the corresponding |
| # initial configuration. |
| my $conf = $self->{'conf'}; |
| foreach my $settable_command ( |
| keys(%Texinfo::Common::document_settable_at_commands)) { |
| if (exists($conf->{$settable_command})) { |
| $self->{'commands_init_conf'}->{$settable_command} |
| = $conf->{$settable_command}; |
| } |
| } |
| |
| # set BODY_ELEMENT_ATTRIBUTES |
| my $default_bcp47_locale = $self->current_bcp47_locale(); |
| |
| # This also sets the language related options to their pre-conversion |
| # values. |
| $self->set_converter_preamble_language_commands(); |
| |
| my $preamble_bcp47_locale = $self->current_bcp47_locale(); |
| |
| if ($preamble_bcp47_locale ne '') { |
| $self->set_conf('BODY_ELEMENT_ATTRIBUTES', 'lang="' |
| .$preamble_bcp47_locale.'"'); |
| } else { |
| #$self->set_conf('BODY_ELEMENT_ATTRIBUTES', 'lang=""'); |
| |
| # Note: HTML 5 documentation specifies that the lang attribute can |
| # take an empty string as its value to specify that the language |
| # is unknown. However, outputting lang="" is unnecessary. |
| } |
| |
| if ($default_bcp47_locale ne $preamble_bcp47_locale) { |
| $self->reset_lang_translation_from_customization( |
| $self->get_conf('documentlanguage'), |
| $self->get_conf('documentscript')); |
| } |
| |
| _init_conversion_after_setup_handler($self); |
| |
| my $jslicenses = {}; |
| if ($self->get_conf('HTML_MATH') |
| and $self->get_conf('HTML_MATH') eq 'mathjax') { |
| # See https://www.gnu.org/licenses/javascript-labels.html |
| |
| my $mathjax_script = $self->get_conf('MATHJAX_SCRIPT'); |
| my $mathjax_source = $self->get_conf('MATHJAX_SOURCE'); |
| |
| $jslicenses->{'mathjax'} = { |
| $mathjax_script => |
| [ 'Apache License, Version 2.0.', |
| 'https://www.apache.org/licenses/LICENSE-2.0', |
| $mathjax_source ]}; |
| } |
| if ($self->get_conf('INFO_JS_DIR')) { |
| $jslicenses->{'infojs'} = { |
| 'js/info.js' => |
| [ 'GNU General Public License 3.0 or later', |
| 'https://www.gnu.org/licenses/gpl-3.0.html', |
| 'js/info.js' ], |
| 'js/modernizr.js' => |
| [ 'Expat', |
| 'http://www.jclark.com/xml/copying.txt', |
| 'js/modernizr.js' ]}; |
| } |
| |
| $self->{'converter_info'}->{'jslicenses'} = $jslicenses; |
| |
| # this sets output_file (based on OUTFILE), to be used if not split, |
| # but also the corresponding 'output_filename' that is useful in |
| # particular when output_file is '', 'destination_directory' that |
| # is mainly useful when split and 'document_name' that is generally useful. |
| my ($output_file, $destination_directory, $output_filename, $document_name) |
| = $self->determine_files_and_directory( |
| $self->get_conf('TEXINFO_OUTPUT_FORMAT')); |
| |
| # set for init files |
| $self->{'converter_info'}->{'document_name'} = $document_name; |
| $self->{'converter_info'}->{'destination_directory'} = $destination_directory; |
| |
| return [$output_file, $destination_directory, $output_filename, |
| $document_name]; |
| } |
| |
| sub _do_jslicenses_file($$) { |
| my ($self, $destination_directory) = @_; |
| |
| my $setting = $self->get_conf('JS_WEBLABELS'); |
| my $path = $self->get_conf('JS_WEBLABELS_FILE'); |
| |
| # Possible settings: |
| # 'generate' - create file at JS_WEBLABELS_FILE |
| # 'reference' - reference file at JS_WEBLABELS_FILE but do not create it |
| # 'omit' - do nothing |
| return if (!defined($setting) or $setting ne 'generate' or !defined($path) |
| or $path eq ''); |
| |
| if (File::Spec->file_name_is_absolute($path) or $path =~ /^[A-Za-z]*:/ |
| or $path eq '-') { |
| $self->converter_document_warn(sprintf( |
| __("cannot use absolute path or URL `%s' for JS_WEBLABELS_FILE when generating web labels file"), $path)); |
| return; |
| } |
| |
| my $doctype = $self->get_conf('DOCTYPE'); |
| $doctype = '' if (!defined($doctype)); |
| my $root_html_element_attributes |
| = _root_html_element_attributes_string($self); |
| my $a = $doctype . "\n" ."<html${root_html_element_attributes}>" |
| .'<head><title>jslicense labels</title></head> |
| <body> |
| <table id="jslicense-labels1"> |
| '; |
| |
| my $jslicenses = $self->get_info('jslicenses'); |
| foreach my $category (sort(keys(%$jslicenses))) { |
| foreach my $file (sort(keys(%{$jslicenses->{$category}}))) { |
| my $file_info = $jslicenses->{$category}->{$file}; |
| $a .= "<tr>\n"; |
| $a .= '<td><a href="'. |
| $self->url_protect_url_text($file)."\">$file</a></td>\n"; |
| $a .= '<td><a href="'.$self->url_protect_url_text($file_info->[1]) |
| ."\">$file_info->[0]</a></td>\n"; |
| $a .= '<td><a href="'.$self->url_protect_url_text($file_info->[2]) |
| ."\">$file_info->[2]</a></td>\n"; |
| $a .= "</tr>\n"; |
| } |
| } |
| |
| $a .= "</table>\n</body></html>\n"; |
| |
| my $license_file; |
| if ($destination_directory ne '') { |
| $license_file = join('/', ($destination_directory, $path)); |
| } else { |
| $license_file = $path; |
| } |
| # sequence of bytes |
| my ($licence_file_path, $path_encoding) |
| = $self->encoded_output_file_name($license_file); |
| my ($fh, $error_message_licence_file, $overwritten_file) |
| = Texinfo::Convert::Utils::output_files_open_out( |
| $self->output_files_information(), |
| $licence_file_path, undef, |
| $self->get_conf('OUTPUT_ENCODING_NAME')); |
| if ($overwritten_file) { |
| $self->converter_document_warn( |
| sprintf(__("overwriting output file with js licences: %s"), |
| $license_file)); |
| } |
| if (defined($fh)) { |
| print $fh $a; |
| Texinfo::Convert::Utils::output_files_register_closed( |
| $self->output_files_information(), $licence_file_path); |
| if (!close ($fh)) { |
| $self->converter_document_error( |
| sprintf(__("error on closing %s: %s"), |
| $license_file, $!)); |
| } |
| } else { |
| $self->converter_document_error( |
| sprintf(__("could not open %s for writing: %s"), |
| $license_file, $error_message_licence_file)); |
| } |
| } |
| |
| sub _do_js_files($$) { |
| my ($self, $destination_directory) = @_; |
| |
| if ($self->get_conf('INFO_JS_DIR')) { |
| my $info_js_dir = $self->get_conf('INFO_JS_DIR'); |
| my $jsdir; |
| if ($destination_directory ne '') { |
| $jsdir = join('/', ($destination_directory, $info_js_dir)); |
| } else { |
| $jsdir = $info_js_dir; |
| } |
| my ($encoded_jsdir, $dir_encoding) |
| = $self->encoded_output_file_name($jsdir); |
| my $succeeded |
| = $self->create_destination_directory($encoded_jsdir, $jsdir); |
| # Copy JS files. |
| if ($succeeded) { |
| if (!$self->get_conf('TEST')) { |
| my $jssrcdir; |
| if (!$Texinfo::ModulePath::texinfo_uninstalled) { |
| $jssrcdir = join('/', ( |
| $Texinfo::ModulePath::converter_datadir, 'js')); |
| } else { |
| $jssrcdir = join('/', ( |
| $Texinfo::ModulePath::t2a_srcdir, $updir, 'js')); |
| } |
| for my $f ('info.js', 'modernizr.js', 'info.css') { |
| my $from = join('/', ($jssrcdir, $f)); |
| |
| if (!copy($from, $jsdir)) { |
| $self->converter_document_error( |
| sprintf(__("error on copying %s into %s"), $from, $jsdir)); |
| } |
| } |
| } else { |
| # create empty files for tests to keep results stable. |
| foreach my $f ('info.js', 'modernizr.js', 'info.css') { |
| my $filename = join('/', ($jsdir, $f)); |
| if (!open(FH, '>', $filename)) { |
| $self->converter_document_error( |
| sprintf(__("error on creating empty %s: %s"), |
| $filename, $!)); |
| } else { |
| if (!close(FH)) { |
| $self->converter_document_error( |
| sprintf(__("error on closing empty %s: %s"), |
| $filename, $!)); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| my $jslicenses = $self->get_info('jslicenses'); |
| if (defined($jslicenses) and scalar(%$jslicenses)) { |
| _do_jslicenses_file($self, $destination_directory); |
| } |
| } |
| |
| # return 0 on failure, 1 on success. |
| sub _finish_output($$$$) { |
| my ($self, $output_file, $destination_directory, $files_source_info) = @_; |
| |
| _do_js_files($self, $destination_directory); |
| |
| my $stage_handlers = $self->{'stage_handlers'}; |
| my $handler_fatal_error_level = $self->get_conf('HANDLER_FATAL_ERROR_LEVEL'); |
| my $finish_status = _run_stage_handlers($self, $stage_handlers, |
| $self->{'document'}, 'finish'); |
| unless ($finish_status < $handler_fatal_error_level |
| and $finish_status > -$handler_fatal_error_level) { |
| return 0; |
| } |
| |
| # undef status means an error occured |
| my $node_redirections_status = _node_redirections($self, $output_file, |
| $destination_directory, $files_source_info); |
| |
| if (!defined($node_redirections_status)) { |
| return 0; |
| } |
| |
| return 1; |
| } |
| |
| # Main function for outputting a manual in HTML. |
| # $SELF is the output converter object of class Texinfo::Convert::HTML (this |
| # module), and $DOCUMENT is the parsed document from the parser and structuring |
| sub output($$) { |
| my ($self, $document) = @_; |
| |
| $self->conversion_initialization('_output', $document); |
| |
| my $paths = _setup_output($self); |
| if (!defined($paths)) { |
| $self->conversion_finalization(); |
| return undef; |
| } |
| my ($output_file, $destination_directory, $output_filename, $document_name) |
| = @$paths; |
| |
| # Get the list of output units to be processed. |
| my ($output_units, $special_units, $associated_special_units) |
| = _prepare_conversion_units($self, $document, $document_name); |
| |
| # setup untranslated strings |
| _translate_names($self); |
| |
| my $files_source_info |
| = _prepare_units_directions_files($self, $output_units, $special_units, |
| $associated_special_units, |
| $output_file, $destination_directory, $output_filename, |
| $document_name); |
| |
| my $succeeded = _prepare_converted_output_info($self, $output_file, |
| $output_filename, $output_units); |
| if (!$succeeded) { |
| $self->conversion_finalization(); |
| return undef; |
| } |
| |
| # conversion |
| my $text_output = _html_convert_output($self, $output_file, |
| $destination_directory, $output_filename, $document_name, |
| $document, $output_units, $special_units); |
| |
| if (!defined($text_output)) { |
| $self->conversion_finalization(); |
| return undef; |
| } |
| |
| if ($text_output ne '' and $output_file eq '') { |
| # $output_file eq '' should always be true, as $text_output is only |
| # filled in that case. |
| if (!$self->get_conf('TEST')) { |
| # This case is unlikely to happen, as there is no output file |
| # only if formatting is called as convert, which only happens in tests. |
| _do_js_files($self, $destination_directory); |
| } |
| $self->conversion_finalization(); |
| return $text_output; |
| } |
| |
| my $finish_succeeded = _finish_output($self, $output_file, |
| $destination_directory, $files_source_info); |
| |
| if (!$finish_succeeded) { |
| $self->conversion_finalization(); |
| return undef; |
| } |
| |
| $self->conversion_finalization(); |
| return undef; |
| } |
| |
| # This is called from the main program on the converter. |
| sub output_internal_links($) { |
| my $self = shift; |
| |
| my $out_string = ''; |
| |
| foreach my $output_unit (@{$self->{'document_units'}}) { |
| my $text; |
| my $href; |
| my $command = $output_unit->{'unit_command'}; |
| if (defined($command)) { |
| # Use '' for filename, to force a filename in href. |
| $href = $self->command_href($command, ''); |
| my ($tree, $in_code) = _internal_command_tree($self, $command, 0); |
| if (defined($tree)) { |
| $text = Texinfo::Convert::Text::convert_to_text($tree, |
| $self->{'convert_text_options'}); |
| } |
| if (defined($href) or defined($text)) { |
| $out_string .= $href if (defined($href)); |
| $out_string .= "\tunit\t"; |
| $out_string .= $text if (defined($text)); |
| $out_string .= "\n"; |
| } |
| } |
| } |
| |
| if (exists($self->{'document'})) { |
| my $sections_list = $self->{'document'}->sections_list(); |
| foreach my $section_relations (@{$sections_list}) { |
| my $command = $section_relations->{'element'}; |
| my $href = $self->command_href($command, ''); |
| my ($tree, $in_code) = _internal_command_tree($self, $command, 0); |
| my $text; |
| if (defined($tree)) { |
| $text = Texinfo::Convert::Text::convert_to_text($tree, |
| $self->{'convert_text_options'}); |
| } |
| if (defined($href) or defined($text)) { |
| $out_string .= $href if (defined($href)); |
| $out_string .= "\tsection\t"; |
| my $command_name |
| = Texinfo::Structuring::section_level_adjusted_command_name($command); |
| $out_string .= $command_name.' '; |
| $out_string .= $text if (defined($text)); |
| $out_string .= "\n"; |
| } |
| } |
| |
| my $labels_list = $self->{'document'}->labels_list(); |
| if (defined($labels_list)) { |
| my %commands_lists; |
| foreach my $target_element (@$labels_list) { |
| next if (not exists($target_element->{'extra'}) |
| or not $target_element->{'extra'}->{'is_target'}); |
| |
| my $cmdname = $target_element->{'cmdname'}; |
| if (!exists($commands_lists{$cmdname})) { |
| $commands_lists{$cmdname} = []; |
| } |
| push @{$commands_lists{$cmdname}}, $target_element; |
| } |
| foreach my $cmdtype ('node', 'anchor', 'namedanchor', 'float') { |
| next unless (exists($commands_lists{$cmdtype})); |
| foreach my $target_element (@{$commands_lists{$cmdtype}}) { |
| my $label_element |
| = Texinfo::Common::get_label_element($target_element); |
| my $href = $self->command_href($target_element, ''); |
| my $text; |
| if (defined($label_element)) { |
| $text = Texinfo::Convert::Text::convert_to_text($label_element, |
| $self->{'convert_text_options'}); |
| } |
| if (defined($href) or defined($text)) { |
| $out_string .= $href if (defined($href)); |
| $out_string .= "\t${cmdtype}\t"; |
| $out_string .= $text if (defined($text)); |
| $out_string .= "\n"; |
| } |
| } |
| } |
| } |
| } |
| |
| my $index_entries_by_letter |
| = $self->get_converter_indices_sorted_by_letter(); |
| if (defined($index_entries_by_letter)) { |
| my $indices_information; |
| if (exists($self->{'document'})) { |
| $indices_information = $self->{'document'}->indices_information(); |
| } |
| |
| foreach my $index_name (sort(keys(%{$index_entries_by_letter}))) { |
| foreach my $letter_entry (@{$index_entries_by_letter->{$index_name}}) { |
| foreach my $index_entry (@{$letter_entry->{'entries'}}) { |
| my $main_entry_element = $index_entry->{'entry_element'}; |
| # does not refer to the document |
| my $seeentry |
| = Texinfo::Common::index_entry_referred_entry($main_entry_element, |
| 'seeentry'); |
| next if (defined($seeentry)); |
| my $seealso |
| = Texinfo::Common::index_entry_referred_entry($main_entry_element, |
| 'seealso'); |
| next if (defined($seealso)); |
| |
| my $href; |
| $href = $self->command_href($main_entry_element, ''); |
| # Obtain term by converting to text |
| my $in_code |
| = $indices_information->{$index_entry->{'index_name'}}->{'in_code'}; |
| if ($in_code) { |
| Texinfo::Convert::Text::set_options_code( |
| $self->{'convert_text_options'}); |
| } |
| my $entry_reference_content_element |
| = Texinfo::Indices::converter_index_content_element( |
| $main_entry_element, $self, 0); |
| my @contents = ($entry_reference_content_element); |
| my $subentries_tree |
| = Texinfo::Convert::Utils::comma_index_subentries_tree( |
| $main_entry_element); |
| if (defined($subentries_tree)) { |
| push @contents, @{$subentries_tree->{'contents'}}; |
| } |
| my $index_term = Texinfo::Convert::Text::convert_to_text( |
| Texinfo::TreeElement::new({'contents' => \@contents}), |
| $self->{'convert_text_options'}); |
| if ($in_code) { |
| Texinfo::Convert::Text::reset_options_code( |
| $self->{'convert_text_options'}); |
| } |
| if (defined($index_term) and $index_term =~ /\S/) { |
| $out_string .= $href if (defined($href)); |
| $out_string .= "\t$index_name\t"; |
| $out_string .= $index_term; |
| $out_string .= "\n"; |
| } |
| } |
| } |
| } |
| } |
| if ($out_string ne '') { |
| return $out_string; |
| } else { |
| return undef; |
| } |
| } |
| |
| 1; |