# Transformations.pm: some transformations of the document tree # # Copyright 2010-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 . # # Original author: Patrice Dumas # Parts (also from Patrice Dumas) come from texi2html.pl. # # ALTIMP perl/XSTexinfo/parser_document/StructuringTransfoXS.xs # ALTIMP C/structuring_transfo/transformations.c package Texinfo::Transformations; use 5.006; use strict; # To check if there is no erroneous autovivification #no autovivification qw(fetch delete exists store strict); use Carp qw(cluck confess); use Texinfo::StructTransfXS; use Texinfo::XSLoader; use Texinfo::Commands; use Texinfo::TreeElement; use Texinfo::Common; use Texinfo::Translations; use Texinfo::Document; use Texinfo::ManipulateTree; use Texinfo::Structuring; require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw( protect_hashchar_at_line_beginning reference_to_arg_in_tree ); our $VERSION = '7.3'; my $XS_structuring = Texinfo::XSLoader::XS_structuring_enabled(); my %XS_overrides = ( "Texinfo::Transformations::fill_gaps_in_sectioning_in_document" => "Texinfo::StructTransfXS::fill_gaps_in_sectioning_in_document", "Texinfo::Transformations::reference_to_arg_in_document" => "Texinfo::StructTransfXS::reference_to_arg_in_document", "Texinfo::Transformations::complete_tree_nodes_menus_in_document" => "Texinfo::StructTransfXS::complete_tree_nodes_menus_in_document", "Texinfo::Transformations::complete_tree_nodes_missing_menu" => "Texinfo::StructTransfXS::complete_tree_nodes_missing_menu", "Texinfo::Transformations::regenerate_master_menu" => "Texinfo::StructTransfXS::regenerate_master_menu", "Texinfo::Transformations::insert_nodes_for_sectioning_commands" => "Texinfo::StructTransfXS::insert_nodes_for_sectioning_commands", "Texinfo::Transformations::protect_hashchar_at_line_beginning_in_document" => "Texinfo::StructTransfXS::protect_hashchar_at_line_beginning_in_document", "Texinfo::Transformations::protect_first_parenthesis_in_targets_in_document" => "Texinfo::StructTransfXS::protect_first_parenthesis_in_targets_in_document", ); our $module_loaded = 0; sub import { if (!$module_loaded) { if ($XS_structuring) { for my $sub (keys %XS_overrides) { Texinfo::XSLoader::override ($sub, $XS_overrides{$sub}); } } $module_loaded = 1; } # The usual import method goto &Exporter::import; } # Add raise/lowersections to be back at the normal level from # the $SECTION level. The raise/lowersections are added at the # end of $PARENT. # If $MODIFIER is set to -1, add raise/lowersections to go from # the normal level to the $SECTION level. sub _correct_level($$;$) { my ($section, $parent, $modifier) = @_; $modifier = 1 if (!defined($modifier)); if (exists($section->{'extra'}) and $section->{'extra'}->{'level_modifier'}) { my $level_to_remove = $modifier * $section->{'extra'}->{'level_modifier'}; my $cmdname; if ($level_to_remove < 0) { $cmdname = 'raisesections'; } else { $cmdname = 'lowersections'; } my $remaining_level = abs($level_to_remove); while ($remaining_level) { my $element = Texinfo::TreeElement::new({'cmdname' => $cmdname, 'parent' => $parent}); push @{$parent->{'contents'}}, $element; my $line_args = Texinfo::TreeElement::new({'type' => 'line_arg', 'info' => {'spaces_after_argument' => Texinfo::TreeElement::new( {'type' => 'spaces_after_argument', 'text' => "\n"})}, 'parent' => $element}); push @{$element->{'contents'}}, $line_args; $remaining_level--; } } } sub fill_gaps_in_sectioning_in_document($;$) { my ($document, $commands_heading_content) = @_; my $root = $document->tree(); my $sections_list = $document->sections_list(); my $contents_nr = scalar(@{$root->{'contents'}}); my @added_sections; # initialize current and next sections my $idx_current_section = -1; my $idx_next_section = -1; my $idx = 0; while ($idx < $contents_nr) { my $content = $root->{'contents'}->[$idx]; if (! exists($content->{'cmdname'}) or $content->{'cmdname'} eq 'node' or ! exists($Texinfo::Commands::root_commands{$content->{'cmdname'}})) { } elsif ($idx_current_section < 0) { $idx_current_section = $idx; } elsif ($idx_next_section < 0) { $idx_next_section = $idx; last; } $idx++; } return undef if ($idx_current_section < 0); return \@added_sections if ($idx_next_section < 0); # index in sections_list my $section_idx = 0; while (1) { my $current_section = $root->{'contents'}->[$idx_current_section]; my $current_section_level = Texinfo::Common::section_level($current_section); my $next_section = $root->{'contents'}->[$idx_next_section]; my $next_section_level = Texinfo::Common::section_level($next_section); if ($next_section_level - $current_section_level > 1) { _correct_level($next_section, $current_section); my @new_sections; while ($next_section_level - $current_section_level > 1) { $current_section_level++; my $new_section = Texinfo::TreeElement::new({'cmdname' => $Texinfo::Common::level_to_structuring_command{'unnumbered'} ->[$current_section_level], 'parent' => $root, }); $new_section->{'info'} = Texinfo::TreeElement::new({'spaces_before_argument' => {'text' => ' ', 'type' => 'spaces_before_argument'}}); my $arguments_line = Texinfo::TreeElement::new({'type' => 'arguments_line', 'parent' => $new_section}); my $line_arg = Texinfo::TreeElement::new({'type' => 'line_arg', 'parent' => $arguments_line, 'info' => {'spaces_after_argument' => Texinfo::TreeElement::new({'text' => "\n", 'type' => 'spaces_after_argument'})}}); $arguments_line->{'contents'} = [$line_arg]; my $line_content; if (defined($commands_heading_content)) { $line_content = Texinfo::ManipulateTree::copy_contentsNonXS( $commands_heading_content); $line_content->{'parent'} = $line_arg; } else { my $asis_command = Texinfo::TreeElement::new({'cmdname' => 'asis', 'parent' => $line_arg}); $asis_command->{'contents'} = [ Texinfo::TreeElement::new({'type' => 'brace_container', 'parent' => $asis_command})]; $line_content = $asis_command; } $line_arg->{'contents'} = [$line_content]; $new_section->{'contents'} = [$arguments_line, Texinfo::TreeElement::new({'type' => 'empty_line', 'text' => "\n"})]; my $new_section_relations = {'element' => $new_section}; splice(@{$sections_list}, $section_idx+1, 0, $new_section_relations); $section_idx++; $new_section->{'extra'} = {'section_number' => $section_idx+1}; push @new_sections, $new_section; } splice (@{$root->{'contents'}}, $idx_current_section+1, 0, @new_sections); $idx_next_section += scalar(@new_sections); $contents_nr += scalar(@new_sections); push @added_sections, @new_sections; _correct_level($next_section, $new_sections[-1], -1); } $idx_current_section = $idx_next_section; $section_idx++; $next_section->{'extra'}->{'section_number'} = $section_idx+1; # find the new next section index $idx_next_section = $idx_current_section +1; while ($idx_next_section < $contents_nr) { my $content = $root->{'contents'}->[$idx_next_section]; if (exists($content->{'cmdname'}) and $content->{'cmdname'} ne 'node' and exists($Texinfo::Commands::root_commands{$content->{'cmdname'}})) { last; } $idx_next_section++; } if ($idx_next_section >= $contents_nr) { last; } } return \@added_sections; } # This converts a reference @-command to simple text using one of the # arguments. This is used to remove reference @-command from # constructed node names trees, as node names cannot contain # reference @-command while there could be some in the tree used in # input for the node name tree. sub _reference_to_arg($$$) { my ($type, $current, $document) = @_; if (exists($current->{'cmdname'}) and exists($Texinfo::Commands::ref_commands{$current->{'cmdname'}}) and exists($current->{'contents'})) { # remove from internal references if (defined($document)) { my $internal_references = $document->internal_references_information(); Texinfo::Common::replace_remove_list_element($internal_references, $current); } my @args_try_order; if ($current->{'cmdname'} eq 'inforef' or $current->{'cmdname'} eq 'link') { @args_try_order = (0, 1, 2); } else { @args_try_order = (0, 1, 2, 4, 3); } foreach my $index (@args_try_order) { if (defined($current->{'contents'}->[$index])) { my $arg = $current->{'contents'}->[$index]; # this will not detect if the arg expands as spaces only, like # @asis{ }, @ , but it is not an issue or could even be considered # as a feature. if (!Texinfo::Common::is_content_empty($arg)) { # avoid the type and spaces by getting only the contents my $result = Texinfo::TreeElement::new({'contents' => $arg->{'contents'}, 'parent' => $current->{'parent'}}); foreach my $content (@{$arg->{'contents'}}) { $content->{'parent'} = $result if (exists($content->{'parent'}));; } $current = undef; return [$result]; } } } $current = undef; return Texinfo::TreeElement::new({'text' => ''}); } else { return undef; } } sub reference_to_arg_in_tree($;$) { my ($tree, $document) = @_; return Texinfo::ManipulateTree::modify_tree($tree, \&_reference_to_arg, $document); } # Has an XS override. Defined to be able to test Perl and XS. Undocumented # on purpose. sub reference_to_arg_in_document($) { my $document = shift; reference_to_arg_in_tree($document->tree(), $document); } # prepare and add a new node as a possible cross reference targets # modifies $document # The $DOCUMENT error_messages is used to register error messages. # Does not matter much, as the code checks that the new node target label does # not exist already, therefore there cannot be any error. sub _new_node($$) { my ($node_tree, $document) = @_; # We protect for all the contexts, as the node name should be # the same in the different contexts, even if some protections # are not needed for the parsing. Also, this way the node tree # can be directly reused in the menus for example, without # additional protection, some parts could be double protected # otherwise, those that are protected with @asis. # # needed in nodes lines, @*ref and in menus with a label $node_tree = Texinfo::ManipulateTree::protect_comma_in_tree($node_tree); # always Texinfo::ManipulateTree::protect_first_parenthesis($node_tree); # in menu entry without label $node_tree = Texinfo::ManipulateTree::protect_colon_in_tree($node_tree); # in menu entry with label $node_tree = Texinfo::ManipulateTree::protect_node_after_label_in_tree($node_tree); $node_tree = reference_to_arg_in_tree($node_tree, $document); my $empty_node = 0; if (!exists($node_tree->{'contents'})) { $node_tree->{'contents'} = [Texinfo::TreeElement::new({'text' => ''})]; $empty_node = 1; } my $comment_at_end; if (exists($node_tree->{'contents'}->[-1]->{'cmdname'}) and ($node_tree->{'contents'}->[-1]->{'cmdname'} eq 'c' or $node_tree->{'contents'}->[-1]->{'cmdname'} eq 'comment')) { $comment_at_end = pop @{$node_tree->{'contents'}}; } my $spaces_after_argument = ''; if (scalar(@{$node_tree->{'contents'}}) > 0 and $node_tree->{'contents'}->[-1]->{'text'} and $node_tree->{'contents'}->[-1]->{'text'} =~ s/(\s+)$//) { $spaces_after_argument = $1; } $spaces_after_argument .= "\n" unless ($spaces_after_argument =~ /\n/ or $comment_at_end); my $appended_number = 0 +$empty_node; my ($node, $normalized); my $identifier_target = $document->labels_information(); while (!defined($node) or (defined($identifier_target) and $identifier_target->{$normalized})) { if (defined($node)) { # remove cycles to release the previous node, which will not be used # and does not appear in the tree. Texinfo::ManipulateTree::tree_remove_parents($node); } $node = Texinfo::TreeElement::new({'cmdname' => 'node', 'extra' => {}}); $node->{'info'} = {'spaces_before_argument' => Texinfo::TreeElement::new({'text' => ' ', 'type' => 'spaces_before_argument'})}; my $arguments_line = Texinfo::TreeElement::new({'type' => 'arguments_line', 'parent' => $node}); $node->{'contents'} = [$arguments_line]; my $node_line_arg = Texinfo::TreeElement::new({'type' => 'line_arg', 'parent' => $arguments_line}); $arguments_line->{'contents'} = [$node_line_arg]; $node_line_arg->{'info'} = {'spaces_after_argument' => Texinfo::TreeElement::new({'text' => $spaces_after_argument, 'type' => 'spaces_after_argument'})}; $node_line_arg->{'info'}->{'comment_at_end'} = $comment_at_end if (defined($comment_at_end)); @{$node_line_arg->{'contents'}} = (@{$node_tree->{'contents'}}); if ($appended_number) { push @{$node_line_arg->{'contents'}}, Texinfo::TreeElement::new({'text' => " $appended_number"}); } foreach my $content (@{$node_line_arg->{'contents'}}) { $content->{'parent'} = $node_line_arg if (exists($content->{'parent'})); } $normalized = Texinfo::Convert::NodeNameNormalization::convert_to_node_identifier( Texinfo::TreeElement::new( { 'contents' => $node_line_arg->{'contents'} })); if ($normalized !~ /[^-]/) { if ($appended_number) { warn "BUG: spaces only node name despite appending $appended_number\n"; return undef; } else { # remove cycles to release this empty node, which is discarded # and does not appear in the tree. Texinfo::ManipulateTree::tree_remove_parents($node); $node = undef; } } $appended_number++; } $node->{'extra'}->{'normalized'} = $normalized; Texinfo::Document::register_label_element($document, $node, $document->{'error_messages'}, $document->get_conf('DEBUG')); return $node; } # reassociate a tree element to the new node, from previous node sub _reassociate_to_node($$$) { my ($type, $current, $argument) = @_; my ($new_node_relations, $previous_node_relations) = @{$argument}; if (exists($current->{'cmdname'}) and $current->{'cmdname'} eq 'menu') { if (defined($previous_node_relations)) { if (!exists($previous_node_relations->{'menus'}) or not scalar(@{$previous_node_relations->{'menus'}}) or not (grep {$current eq $_} @{$previous_node_relations->{'menus'}})) { print STDERR "BUG: menu $current not in previous node $previous_node_relations->{'element'}\n"; } else { @{$previous_node_relations->{'menus'}} = grep {$_ ne $current} @{$previous_node_relations->{'menus'}}; delete $previous_node_relations->{'menus'} if (!scalar(@{$previous_node_relations->{'menus'}})); } } push @{$new_node_relations->{'menus'}}, $current; } elsif (exists($current->{'extra'}) and exists($current->{'extra'}->{'element_node'})) { if (defined($previous_node_relations)) { my $previous_node = $previous_node_relations->{'element'}; if ($current->{'extra'}->{'element_node'} ne $previous_node->{'extra'}->{'normalized'}) { print STDERR "Bug: element $current not in previous node $previous_node; " .Texinfo::Common::debug_print_element($current)."\n"; print STDERR " previous node: " .Texinfo::Convert::Texinfo::root_heading_command_to_texinfo($previous_node)."\n"; print STDERR " current node identifier: ". $current->{'extra'}->{'element_node'}."\n"; } } $current->{'extra'}->{'element_node'} = $new_node_relations->{'element'}->{'extra'}->{'normalized'}; } elsif (exists($current->{'cmdname'}) and $current->{'cmdname'} eq 'nodedescription') { if (!exists($new_node_relations->{'node_description'})) { $new_node_relations->{'node_description'} = $current; } if (defined($previous_node_relations) and exists($previous_node_relations->{'node_description'}) and $previous_node_relations->{'node_description'} eq $current) { delete $previous_node_relations->{'node_description'}; } } elsif (exists($current->{'cmdname'}) and $current->{'cmdname'} eq 'nodedescriptionblock') { if (!exists($new_node_relations->{'node_long_description'})) { $new_node_relations->{'node_long_description'} = $current; } if (defined($previous_node_relations) and exists($previous_node_relations->{'node_long_description'}) and $previous_node_relations->{'node_long_description'} eq $current) { delete $previous_node_relations->{'node_long_description'}; } } return undef; } sub insert_nodes_for_sectioning_commands($) { my $document = shift; my $root = $document->tree(); my $nodes_list = $document->nodes_list(); my $sections_list = $document->sections_list(); # this is not used in the function. The call makes sure that the C code # considers that the C data is up to date and do not attempts to rebuild # from C afterwards and instead returns the Perl data. This is important # because the Perl labels_list is modified in _new_node, not the C data, # such that the C data is not up to date and should not be accessed again. my $labels_list = $document->labels_list(); my @added_nodes; my $previous_node_relations; my $contents_nr = scalar(@{$root->{'contents'}}); my $node_idx = 0; for (my $idx = 0; $idx < $contents_nr; $idx++) { my $content = $root->{'contents'}->[$idx]; if (exists($content->{'cmdname'}) and $content->{'cmdname'} ne 'node' and $content->{'cmdname'} ne 'part' and exists($Texinfo::Commands::root_commands{$content->{'cmdname'}})) { my $section_relations = $sections_list->[$content->{'extra'}->{'section_number'} -1]; if ($section_relations->{'associated_node'}) { next; } my $new_node_tree; if ($content->{'cmdname'} eq 'top') { $new_node_tree = Texinfo::TreeElement::new({'contents' => [ Texinfo::TreeElement::new({'text' => 'Top'})]}); } else { my $arguments_line = $content->{'contents'}->[0]; my $line_arg = $arguments_line->{'contents'}->[0]; $new_node_tree = Texinfo::ManipulateTree::copy_contentsNonXS($line_arg); } my $new_node = _new_node($new_node_tree, $document); if (defined($new_node)) { # insert before $content splice(@{$root->{'contents'}}, $idx, 0, $new_node); $idx++; $contents_nr++; # insert in nodes list my $new_node_relations = {'element' => $new_node, 'associated_section' => $section_relations}; splice(@{$nodes_list}, $node_idx, 0, $new_node_relations); $node_idx++; $new_node->{'extra'}->{'node_number'} = $node_idx; $section_relations->{'associated_node'} = $new_node_relations; $new_node->{'parent'} = $content->{'parent'} if (exists($content->{'parent'})); push @added_nodes, $new_node; # reassociate index entries and menus Texinfo::ManipulateTree::modify_tree($content, \&_reassociate_to_node, [$new_node_relations, $previous_node_relations]); } } # check is_target to avoid erroneous nodes, such as duplicates if (exists($content->{'cmdname'}) and $content->{'cmdname'} eq 'node' and exists($content->{'extra'}) and $content->{'extra'}->{'is_target'}) { $previous_node_relations = $nodes_list->[$node_idx]; # debug if ($previous_node_relations->{'element'} ne $content) { confess("insert_nodes_for_sectioning_commands: wrong node: '" .$previous_node_relations->{'element'}->{'extra'}->{'normalized'}. "' '".$content->{'extra'}->{'normalized'}."'\n"); } $node_idx++; # reset node index taking into account the added nodes $content->{'extra'}->{'node_number'} = $node_idx; } } return \@added_nodes; } sub _prepend_new_menu_in_node_section($$$) { my ($node_relations, $section, $current_menu) = @_; if (not defined($current_menu)) { cluck "input menu undef"; } push @{$section->{'contents'}}, $current_menu; $current_menu->{'parent'} = $section; push @{$section->{'contents'}}, Texinfo::TreeElement::new({'type' => 'empty_line', 'text' => "\n",}); push @{$node_relations->{'menus'}}, $current_menu; } sub _complete_node_menu($;$) { my ($node_relations, $use_sections) = @_; my @node_childs = Texinfo::Structuring::get_node_node_childs_from_sectioning( $node_relations); if (scalar(@node_childs)) { my %existing_entries; if (exists($node_relations->{'menus'}) and scalar(@{$node_relations->{'menus'}})) { foreach my $menu (@{$node_relations->{'menus'}}) { foreach my $entry (@{$menu->{'contents'}}) { if (exists($entry->{'type'}) and $entry->{'type'} eq 'menu_entry') { my $normalized_entry_node = Texinfo::ManipulateTree::normalized_menu_entry_internal_node( $entry); if (defined($normalized_entry_node)) { $existing_entries{$normalized_entry_node} = [$menu, $entry]; } } } } } #print STDERR "existing_entries: ".join('|', keys(%existing_entries))."\n"; my @pending; my $current_menu; foreach my $node_entry_relations (@node_childs) { my $node_entry = $node_entry_relations->{'element'}; my $normalized = $node_entry->{'extra'}->{'normalized'}; if (exists($existing_entries{$normalized})) { my $entry; ($current_menu, $entry) = @{$existing_entries{$normalized}}; if (scalar(@pending)) { my $index; for ($index = 0; $index < scalar(@{$current_menu->{'contents'}}); $index++) { #print STDERR "$index, ".scalar(@{$current_menu->{'contents'}})."\n"; last if ($current_menu->{'contents'}->[$index] eq $entry); } splice (@{$current_menu->{'contents'}}, $index, 0, @pending); foreach my $pending_entry (@pending) { $pending_entry->{'parent'} = $current_menu; } @pending = (); } } else { my $entry = Texinfo::Structuring::new_node_menu_entry($node_entry_relations, $use_sections); # not defined $entry should mean an empty node. We do not warn as # we try, in general, to be silent in the transformations. push @pending, $entry if (defined($entry)); } } if (scalar(@pending)) { if (!defined($current_menu)) { my $section = $node_relations->{'associated_section'}->{'element'}; $current_menu = Texinfo::TreeElement::new({'contents' => \@pending, 'parent' => $section}); Texinfo::Structuring::new_block_command($current_menu, 'menu'); _prepend_new_menu_in_node_section($node_relations, $section, $current_menu); } else { if (exists($current_menu->{'contents'}->[-1]->{'cmdname'}) and $current_menu->{'contents'}->[-1]->{'cmdname'} eq 'end') { splice (@{$current_menu->{'contents'}}, -1, 0, @pending); } else { # Should probably only happen with menu without end push @{$current_menu->{'contents'}}, @pending; } } foreach my $entry (@pending) { $entry->{'parent'} = $current_menu; } } } } sub _get_non_automatic_nodes_with_sections($) { my $document = shift; my $root = $document->tree(); my $nodes_list = $document->nodes_list(); my @non_automatic_nodes; foreach my $node_relations (@{$nodes_list}) { my $node_element = $node_relations->{'element'}; if (not (scalar(@{$node_element->{'contents'}->[0]->{'contents'}}) > 1) and exists($node_relations->{'associated_section'})) { push @non_automatic_nodes, $node_relations; } } return [ @non_automatic_nodes ]; } # This should be called after Texinfo::Structuring::sectioning_structure. sub complete_tree_nodes_menus_in_document($;$) { my ($document, $use_sections) = @_; my $non_automatic_nodes = _get_non_automatic_nodes_with_sections($document); foreach my $node_relations (@{$non_automatic_nodes}) { _complete_node_menu($node_relations, $use_sections); } } # this only complete menus if there was no menu # The document is used to pass customization information for the gdt() call. sub complete_tree_nodes_missing_menu($;$) { my ($document, $use_sections) = @_; my $lang_translations = Texinfo::Translations::new_lang_translation( $document->get_conf('documentlanguage'), $document->get_conf('COMMAND_LINE_ENCODING')); my $debug = $document->get_conf('DEBUG'); my $non_automatic_nodes = _get_non_automatic_nodes_with_sections($document); foreach my $node_relations (@{$non_automatic_nodes}) { if (not exists($node_relations->{'menus'}) or not scalar(@{$node_relations->{'menus'}})) { my $current_menu = Texinfo::Structuring::new_complete_node_menu($node_relations, $lang_translations, $debug, $use_sections); if (defined($current_menu)) { my $section = $node_relations->{'associated_section'}->{'element'}; _prepend_new_menu_in_node_section($node_relations, $section, $current_menu); } } } } # The document is passed as customization information sub regenerate_master_menu($;$) { my ($document, $use_sections) = @_; my $identifier_target = $document->labels_information(); my $nodes_list = $document->nodes_list(); my $top_node = $identifier_target->{'Top'}; return undef if (!defined($top_node)); my $top_node_relations = $nodes_list->[$top_node->{'extra'}->{'node_number'} -1]; return undef if (!exists($top_node_relations->{'menus'}) or !scalar(@{$top_node_relations->{'menus'}})); my $lang_translation = Texinfo::Translations::new_lang_translation( $document->get_conf('documentlanguage'), $document->get_conf('COMMAND_LINE_ENCODING')); my $new_detailmenu = Texinfo::Structuring::new_detailmenu( $lang_translation, undef, $document, $identifier_target, $nodes_list, $top_node_relations->{'menus'}, $document->get_conf('DEBUG'), $use_sections); # no need for a master menu return undef if (!defined($new_detailmenu)); my $global_detailmenu = $document->global_commands_information()->{'detailmenu'}; foreach my $menu (@{$top_node_relations->{'menus'}}) { my $menu_contents_len = scalar(@{$menu->{'contents'}}); for (my $current_idx = 0; $current_idx < $menu_contents_len; $current_idx++) { my $entry = $menu->{'contents'}->[$current_idx]; if (exists($entry->{'cmdname'}) and $entry->{'cmdname'} eq 'detailmenu') { # replace existing detailmenu by the master menu $new_detailmenu->{'parent'} = $menu; splice (@{$menu->{'contents'}}, $current_idx, 1, $new_detailmenu); # also replace in global commands Texinfo::Common::replace_remove_list_element($global_detailmenu, $entry, $new_detailmenu); # NOTE the menu entries added in @detailmenu are not added as # internal references. However, this is not an issue, as the # menu entries in @detailmenu are also in regular menus. # As long as internal references are only used to check if all # the nodes are referenced, not having @detailmenu entries # added is not an issue at all. # remove internal refs of removed entries my $internal_references = $document->internal_references_information(); foreach my $detailmenu_entry (@{$entry->{'contents'}}) { if (exists($detailmenu_entry->{'type'}) and $detailmenu_entry->{'type'} eq 'menu_entry') { foreach my $entry_content (@{$detailmenu_entry->{'contents'}}) { if (exists($entry_content->{'type'}) and $entry_content->{'type'} eq 'menu_entry_node') { Texinfo::Common::replace_remove_list_element( $internal_references, $entry_content); } } } } return 1; } } } my $last_menu = $top_node_relations->{'menus'}->[-1]; my $index = scalar(@{$last_menu->{'contents'}}); if ($index and $last_menu->{'contents'}->[$index-1]->{'cmdname'} and $last_menu->{'contents'}->[$index-1]->{'cmdname'} eq 'end') { $index--; } $new_detailmenu->{'parent'} = $last_menu; if ($index) { my $last_element = $last_menu->{'contents'}->[$index-1]; if (exists($last_element->{'type'}) and $last_element->{'type'} eq 'menu_comment' and scalar(@{$last_element->{'contents'}}) and exists($last_element->{'contents'}->[-1]->{'type'}) and $last_element->{'contents'}->[-1]->{'type'} eq 'preformatted') { { # already a menu comment at the end of the menu, add an empty line my $preformatted = $last_element->{'contents'}->[-1]; my $empty_line = Texinfo::TreeElement::new({'type' => 'empty_line', 'text' => "\n",}); push @{$preformatted->{'contents'}}, $empty_line; } } elsif (exists($last_element->{'type'}) and $last_element->{'type'} eq 'menu_entry') { # there is a last menu entry, add a menu comment containing an empty line # after it my $menu_comment = Texinfo::TreeElement::new({'type' => 'menu_comment', 'parent' => $last_menu}); splice (@{$last_menu->{'contents'}}, $index, 0, $menu_comment); $index++; my $preformatted = Texinfo::TreeElement::new({'type' => 'preformatted', 'parent' => $menu_comment}); push @{$menu_comment->{'contents'}}, $preformatted; my $empty_line = Texinfo::TreeElement::new( {'type' => 'after_menu_description_line', 'text' => "\n",}); push @{$preformatted->{'contents'}}, $empty_line; } } # insert master menu splice (@{$last_menu->{'contents'}}, $index, 0, $new_detailmenu); push @$global_detailmenu, $new_detailmenu; return 1; } # modify the menu tree to put description and menu comment content # together directly in the menu. Put the menu_entry in a preformatted. # last merge preformatted. sub menu_to_simple_menu($); sub menu_to_simple_menu($) { my $menu = shift; my @contents; foreach my $content (@{$menu->{'contents'}}) { if (exists($content->{'type'}) and $content->{'type'} eq 'menu_comment') { push @contents, @{$content->{'contents'}}; } elsif (exists($content->{'type'}) and $content->{'type'} eq 'menu_entry') { my $preformatted = {'type' => 'preformatted', 'contents' => [$content]}; push @contents, $preformatted; $content->{'parent'} = $preformatted; my $in_description; my @args = @{$content->{'contents'}}; @{$content->{'contents'}} = (); while (@args) { if (exists($args[0]->{'type'}) and $args[0]->{'type'} eq 'menu_entry_description') { my $description = shift @args; push @contents, @{$description->{'contents'}}; push @contents, @args; last; } else { my $arg = shift @args; push @{$content->{'contents'}}, $arg; } } } elsif (exists($content->{'cmdname'}) and exists($Texinfo::Commands::block_commands{$content->{'cmdname'}}) and $Texinfo::Commands::block_commands{$content->{'cmdname'}} eq 'menu') { menu_to_simple_menu($content); push @contents, $content; } else { push @contents, $content; } } # reset parent, put in menu and merge preformatted. @{$menu->{'contents'}} = (); my $current_preformatted; foreach my $content (@contents) { $content->{'parent'} = $menu; if (exists($content->{'type'}) and $content->{'type'} eq 'preformatted') { if (!defined($current_preformatted)) { $current_preformatted = $content; push @{$menu->{'contents'}}, $content; } else { foreach my $preformatted_content (@{$content->{'contents'}}) { push @{$current_preformatted->{'contents'}}, $preformatted_content; $preformatted_content->{'parent'} = $current_preformatted; } } } else { $current_preformatted = undef; push @{$menu->{'contents'}}, $content; } } } sub _protect_hashchar_at_line_beginning($$$) { my ($type, $parent, $argument) = @_; my $document = $argument; return undef if (exists($parent->{'text'}) or !exists($parent->{'contents'})); my $parent_contents_nr = scalar(@{$parent->{'contents'}}); for (my $i = 0; $i < $parent_contents_nr; $i++) { my $current = $parent->{'contents'}->[$i]; if (exists($current->{'text'}) and $current->{'text'} =~ /^\s*#\s*(line)? (\d+)(( "([^"]+)")(\s+\d+)*)?\s*$/) { # protect if first in container, or if after a newline if ($i == 0 or ($i == 1 and exists($parent->{'contents'}->[0]->{'type'}) and $parent->{'contents'}->[0]->{'type'} eq 'arguments_line') or (exists($parent->{'contents'}->[$i-1]->{'text'}) and $parent->{'contents'}->[$i-1]->{'text'} =~ /\n$/)) { # do not actually protect in raw block command, but warn if (exists($current->{'type'}) and $current->{'type'} eq 'raw') { my $parent_for_warn = $parent; while ($parent_for_warn) { if (exists($parent_for_warn->{'cmdname'}) and exists($parent_for_warn->{'source_info'})) { if (defined($document)) { $document->document_line_warn(sprintf(__( "could not protect hash character in \@%s"), $parent_for_warn->{'cmdname'}), $parent_for_warn->{'source_info'}, 0); } last; } $parent_for_warn = $parent_for_warn->{'parent'}; } } else { my $remaining_source_marks; my $current_position = 0; if (exists($current->{'source_marks'})) { $remaining_source_marks = [@{$current->{'source_marks'}}]; delete $current->{'source_marks'}; } $current->{'text'} =~ s/^(\s*)#//; my $e = Texinfo::TreeElement::new({'text' => $1,}); $current_position = Texinfo::Common::relocate_source_marks( $remaining_source_marks, $e, $current_position, length($1)); if ($e->{'text'} ne '' or $e->{'source_marks'}) { splice @{$parent->{'contents'}}, $i, 0, $e; $i++; $parent_contents_nr++; } $e = Texinfo::TreeElement::new({'cmdname' => 'hashchar', 'parent' => $parent}); my $arg = Texinfo::TreeElement::new({'type' => 'brace_container', 'parent' => $e}); $e->{'contents'} = [$arg]; $current_position = Texinfo::Common::relocate_source_marks( $remaining_source_marks, $e, $current_position, 1); splice @{$parent->{'contents'}}, $i, 0, $e; $i++; $parent_contents_nr++; $current_position = Texinfo::Common::relocate_source_marks( $remaining_source_marks, $current, $current_position, length($current->{'text'})); } } } } return undef; } sub protect_hashchar_at_line_beginning($;$) { my ($tree, $document) = @_; return Texinfo::ManipulateTree::modify_tree($tree, \&_protect_hashchar_at_line_beginning, $document); } # Has an XS override. Defined to be able to test Perl and XS. Undocumented # on purpose. sub protect_hashchar_at_line_beginning_in_document($) { my $document = shift; protect_hashchar_at_line_beginning($document->tree(), $document); return; } sub _protect_first_parenthesis_in_targets($$$) { my ($type, $current, $argument) = @_; my $element_label = Texinfo::Common::get_label_element($current); if (defined($element_label) and $element_label ne '') { Texinfo::ManipulateTree::protect_first_parenthesis($element_label); } return undef; } # Used in Pod::Simple::Texinfo # TODO document sub protect_first_parenthesis_in_targets($) { my $tree = shift; Texinfo::ManipulateTree::modify_tree($tree, \&_protect_first_parenthesis_in_targets); } # Has an XS override. Defined to be able to test Perl and XS. Undocumented # on purpose. sub protect_first_parenthesis_in_targets_in_document($) { my $document = shift; protect_first_parenthesis_in_targets($document->tree()); return; } 1; __END__ =head1 NAME Texinfo::Transformations - transformations of Texinfo tree =head1 NOTES The Texinfo Perl module main purpose is to be used in C to convert Texinfo to other formats. There is no promise of API stability. =head1 DESCRIPTION Includes miscellaneous methods such as as C that adds nodes for sectioning commands without nodes and C and C that completes the node menus based on the sectioning tree. Methods for copying and modifying the Texinfo tree used for default conversion to output formats are in L. =head1 METHODS No method is exported in the default case. =over =item complete_tree_nodes_menus_in_document($document, $add_section_names_in_entries) X> Add menu entries or whole menus for nodes associated with sections, based on the sectioning tree. If the optional C<$add_section_names_in_entries> argument is set, a menu entry name is added using the section name. This function should be called after L. =item complete_tree_nodes_missing_menu($document, $use_section_names_in_entries) X> Add whole menus for nodes associated with sections and without menu, based on the I<$document> sectioning tree. If the optional I<$add_section_names_in_entries> argument is set, a menu entry name is added using the section name. This function should be called after L. =item fill_gaps_in_sectioning_in_document($document, $commands_heading_tree) X> This function adds empty C<@unnumbered> and similar commands in a I<$document> tree to fill gaps in sectioning. This may be used, for example, when converting from a format that can handle gaps in sectioning. In the default case, the added sectioning commands headings are empty. It is possible to use instead the I<$commands_heading_tree> Texinfo tree element. If the sectioning commands are lowered or raised (with C<@raisesections>, C<@lowersection>) the tree may be modified with C<@raisesections> or C<@lowersection> added to some tree elements. =item insert_nodes_for_sectioning_commands($document) X> Insert nodes for sectioning commands without node in C<$document> tree. =item menu_to_simple_menu($menu) X> C transforms the tree of a menu tree element. A simple menu has no I, I or I container anymore, their content are merged directly in the menu in I container. Note that this kind of tree is not supported by other codes, so this transformation should be avoided unless one knows exactly what to expect. =item protect_hashchar_at_line_beginning($tree, $document) X> Protect hash (#) character at the beginning of line such that they would not be considered as lines to be processed by the CPP processor. The I<$document> argument is optional. If defined, the I<$document> is used for error reporting in case an hash character could not be protected because it appeared in a raw formatted environment (C<@tex>, C<@html>...). =item $modified_tree = reference_to_arg_in_tree($tree, $document) X> Modify I<$tree> by converting reference @-commands to simple text using one of the arguments. This transformation can be used, for example, to remove reference @-command from constructed node names trees, as node names cannot contain reference @-command while there could be some in the tree used in input for the node name tree. The I<$document> argument is optional. If given, the converted reference @-command is removed from the I<$document> internal references list. A I<$modified_tree> is not systematically returned, if the I<$tree> in argument is not replaced, undef may also be returned. =item regenerate_master_menu($document, $use_sections) X> Regenerate the I<$document> Top node master menu, replacing the first detailmenu in Top node menus or appending at the end of the Top node menu. I<$use_sections> is an optional argument. If set, sections associated with nodes are used as labels in the generated master menu. =back =head1 SEE ALSO L, L, L. =head1 AUTHOR Patrice Dumas, Ebug-texinfo@gnu.orgE =head1 COPYRIGHT AND LICENSE Copyright 2010- Free Software Foundation, Inc. See the source file for all copyright years. This library 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. =cut