# Transformations.pm: some transformations of the document tree # # Copyright 2010-2024 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. 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::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.2'; my $XS_structuring = Texinfo::XSLoader::XS_structuring_enabled(); our %XS_overrides = ( "Texinfo::Transformations::fill_gaps_in_sectioning" => "Texinfo::StructTransfXS::fill_gaps_in_sectioning", "Texinfo::Transformations::reference_to_arg_in_tree" => "Texinfo::StructTransfXS::reference_to_arg_in_tree", "Texinfo::Transformations::complete_tree_nodes_menus" => "Texinfo::StructTransfXS::complete_tree_nodes_menus", "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" => "Texinfo::StructTransfXS::protect_hashchar_at_line_beginning", "Texinfo::Transformations::protect_first_parenthesis_in_targets" => "Texinfo::StructTransfXS::protect_first_parenthesis_in_targets", ); 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 = shift; my $parent = shift; my $modifier = shift; $modifier = 1 if (!defined($modifier)); if ($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 = {'cmdname' => $cmdname, 'parent' => $parent}; push @{$parent->{'contents'}}, $element; my $rawline_arg = {'type' => 'rawline_arg', 'text' => "\n", 'parent' => $element}; push @{$element->{'args'}}, $rawline_arg; $remaining_level--; } } } sub fill_gaps_in_sectioning($;$) { my $root = shift; my $commands_heading_content = shift; 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 (! $content->{'cmdname'} or $content->{'cmdname'} eq 'node' or ! $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); 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 = {'cmdname' => $Texinfo::Common::level_to_structuring_command{'unnumbered'} ->[$current_section_level], 'parent' => $root, }; $new_section->{'info'} = {'spaces_before_argument' => {'text' => ' ',}}; my $line_arg = {'type' => 'line_arg', 'parent' => $new_section, 'info' => {'spaces_after_argument' => {'text' => "\n",}}}; $new_section->{'args'} = [$line_arg]; my $line_content; if ($commands_heading_content) { $line_content = Texinfo::ManipulateTree::copy_contentsNonXS( $commands_heading_content); $line_content->{'parent'} = $line_arg; } else { my $asis_command = {'cmdname' => 'asis', 'parent' => $line_arg}; $asis_command->{'args'} = [{'type' => 'brace_container', 'parent' => $asis_command}]; $line_content = $asis_command; } $line_arg->{'contents'} = [$line_content]; $new_section->{'contents'} = [{'type' => 'empty_line', 'text' => "\n", 'parent' => $new_section}]; 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; # 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 ($content->{'cmdname'} and $content->{'cmdname'} ne 'node' and $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 = shift; my $current = shift; my $document = shift; if ($current->{'cmdname'} and $Texinfo::Commands::ref_commands{$current->{'cmdname'}} and $current->{'args'}) { # remove from internal references if ($document) { my $internal_references = $document->internal_references_information(); Texinfo::Common::remove_from_array($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->{'args'}->[$index])) { my $arg = $current->{'args'}->[$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 = {'contents' => $arg->{'contents'}, 'parent' => $current->{'parent'}}; foreach my $content (@{$arg->{'contents'}}) { $content->{'parent'} = $result; } return [$result]; } } } return {'text' => '', 'parent' => $current->{'parent'}}; } else { return undef; } } sub reference_to_arg_in_tree($;$) { my $tree = shift; my $document = shift; return Texinfo::ManipulateTree::modify_tree($tree, \&_reference_to_arg, $document); } # prepare and add a new node as a possible cross reference targets # modifies $document # $CUSTOMIZATION_INFORMATION is used for error reporting, but it may # not be useful, as the code checks that the new node target label does # not exist already. # Right now the $DOCUMENT error messages registrar is used to register # error messages, instead it could be given as argument. Does not matter # much, see just above. sub _new_node($$;$) { my $node_tree = shift; my $document = shift; my $customization_information = shift; if ($XS_structuring and $Texinfo::StructTransfXS::XS_package) { # If there were XS overrides for all the transformations, they would # necessarily fail, so treat as a bug even though it does not matter # with missing overrides, as seen just below. if (!$node_tree->{'tree_document_descriptor'}) { print STDERR "BUG: new_node: with XS, no tree_document_descriptor\n"; } # If changes are only done to underlying XS tree, the changes by # Texinfo::Common::protect_* will be done on the underlying XS tree, # but the perl tree will not change, although it is the perl tree # that is used to construct the node. # Also protect_first_parenthesis has no XS override, only # protect_first_parenthesis_in_targets. confess("BUG: _new_node: XS not supported"); # It could have been possible to rebuild the tree, after adding # an override for protect_first_parenthesis. # If XS is used, however, it is be much better to override all # the functions calling the current function instead of trying # to have it work through an interface because the function is # mostly internal. So, it is better to keep failing with XS. # # The issue arise because the current function is used in tests, # this cannot work with XS (and tests are skipped). } # 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 (!$node_tree->{'contents'} or !scalar(@{$node_tree->{'contents'}})) { $node_tree->{'contents'} = [{'text' => ''}]; $empty_node = 1; } my $comment_at_end; if ($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 ($identifier_target and $identifier_target->{$normalized})) { $node = {'cmdname' => 'node', 'extra' => {}}; $node->{'info'} = {'spaces_before_argument' => {'text' => ' '}}; my $node_line_arg = {'type' => 'line_arg', 'parent' => $node}; $node->{'args'} = [$node_line_arg]; $node_line_arg->{'info'} = {'spaces_after_argument' => {'text' => $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'}}, {'text' => " $appended_number"}; } foreach my $content (@{$node_line_arg->{'contents'}}) { $content->{'parent'} = $node_line_arg; } $normalized = Texinfo::Convert::NodeNameNormalization::convert_to_identifier( { 'contents' => $node_line_arg->{'contents'} }); if ($normalized !~ /[^-]/) { if ($appended_number) { warn "BUG: spaces only node name despite appending $appended_number\n"; return undef; } else { $node = undef; } } $appended_number++; } $node->{'extra'}->{'normalized'} = $normalized; my $debug; if ($customization_information) { $debug = $customization_information->get_conf('DEBUG'); } Texinfo::Document::register_label_element($document, $node, $document->registrar(), $debug); return $node; } # reassociate a tree element to the new node, from previous node sub _reassociate_to_node($$$) { my $type = shift; my $current = shift; my $argument = shift; my ($new_node, $previous_node) = @{$argument}; if ($current->{'cmdname'} and $current->{'cmdname'} eq 'menu') { if ($previous_node) { if (not $previous_node->{'extra'} or not defined($previous_node->{'extra'}->{'menus'}) or not scalar(@{$previous_node->{'extra'}->{'menus'}}) or not (grep {$current eq $_} @{$previous_node->{'extra'}->{'menus'}})) { print STDERR "BUG: menu $current not in previous node $previous_node\n"; } else { @{$previous_node->{'extra'}->{'menus'}} = grep {$_ ne $current} @{$previous_node->{'extra'}->{'menus'}}; delete $previous_node->{'extra'}->{'menus'} if !(@{$previous_node->{'extra'}->{'menus'}}); } } push @{$new_node->{'extra'}->{'menus'}}, $current; } elsif ($current->{'extra'} and $current->{'extra'}->{'element_node'}) { if ($previous_node and $current->{'extra'}->{'element_node'} ne $previous_node) { 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: ". Texinfo::Convert::Texinfo::root_heading_command_to_texinfo( $current->{'extra'}->{'element_node'})."\n"; } $current->{'extra'}->{'element_node'} = $new_node; } return undef; } sub insert_nodes_for_sectioning_commands($) { my $document = shift; my $customization_information = $document; my $root = $document->tree(); my @added_nodes; my $previous_node; my $contents_nr = scalar(@{$root->{'contents'}}); for (my $idx = 0; $idx < $contents_nr; $idx++) { my $content = $root->{'contents'}->[$idx]; if ($content->{'cmdname'} and $content->{'cmdname'} ne 'node' and $content->{'cmdname'} ne 'part' and $Texinfo::Commands::root_commands{$content->{'cmdname'}} and not ($content->{'extra'} and $content->{'extra'}->{'associated_node'})) { my $new_node_tree; if ($content->{'cmdname'} eq 'top') { $new_node_tree = {'contents' => [{'text' => 'Top'}]}; } else { $new_node_tree = Texinfo::ManipulateTree::copy_contentsNonXS($content->{'args'}->[0]); } my $new_node = _new_node($new_node_tree, $document, $customization_information); if (defined($new_node)) { # insert before $content splice(@{$root->{'contents'}}, $idx, 0, $new_node); $idx++; $contents_nr++; push @added_nodes, $new_node; $new_node->{'extra'}->{'associated_section'} = $content; $content->{'extra'} = {} if (!$content->{'extra'}); $content->{'extra'}->{'associated_node'} = $new_node; $new_node->{'parent'} = $content->{'parent'}; # reassociate index entries and menus Texinfo::ManipulateTree::modify_tree($content, \&_reassociate_to_node, [$new_node, $previous_node]); } } # check is_target to avoid erroneous nodes, such as duplicates $previous_node = $content if ($content->{'cmdname'} and $content->{'cmdname'} eq 'node' and $content->{'extra'} and $content->{'extra'}->{'is_target'}); } return \@added_nodes; } sub _prepend_new_menu_in_node_section($$$) { my $node = shift; my $section = shift; my $current_menu = shift; if (not defined($current_menu)) { cluck "input menu undef"; } push @{$section->{'contents'}}, $current_menu; $current_menu->{'parent'} = $section; push @{$section->{'contents'}}, {'type' => 'empty_line', 'text' => "\n", 'parent' => $section}; push @{$node->{'extra'}->{'menus'}}, $current_menu; } sub complete_node_menu($;$) { my $node = shift; my $use_sections = shift; my @node_childs = Texinfo::Structuring::get_node_node_childs_from_sectioning($node); if (scalar(@node_childs)) { my %existing_entries; if ($node->{'extra'} and $node->{'extra'}->{'menus'} and @{$node->{'extra'}->{'menus'}}) { foreach my $menu (@{$node->{'extra'}->{'menus'}}) { foreach my $entry (@{$menu->{'contents'}}) { if ($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 (@node_childs) { if ($node_entry->{'extra'} and defined($node_entry->{'extra'}->{'normalized'}) and $existing_entries{$node_entry->{'extra'}->{'normalized'}}) { my $entry; ($current_menu, $entry) = @{$existing_entries{$node_entry->{'extra'}->{'normalized'}}}; if (@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, $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 (!$current_menu) { my $section = $node->{'extra'}->{'associated_section'}; $current_menu = {'contents' => \@pending, 'parent' => $section}; Texinfo::Structuring::new_block_command($current_menu, 'menu'); _prepend_new_menu_in_node_section($node, $section, $current_menu); } else { if ($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 $root = shift; my @non_automatic_nodes; foreach my $content (@{$root->{'contents'}}) { if ($content->{'cmdname'} and $content->{'cmdname'} eq 'node' and not ($content->{'args'} and scalar(@{$content->{'args'}}) > 1) and $content->{'extra'} and $content->{'extra'}->{'associated_section'}) { push @non_automatic_nodes, $content; } } return [ @non_automatic_nodes ]; } # This should be called after Texinfo::Structuring::sectioning_structure. sub complete_tree_nodes_menus($;$) { my $root = shift; my $use_sections = shift; my $non_automatic_nodes = _get_non_automatic_nodes_with_sections($root); foreach my $node (@{$non_automatic_nodes}) { complete_node_menu($node, $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 = shift; my $use_sections = shift; my $customization_information = $document; my $root = $document->tree(); my $non_automatic_nodes = _get_non_automatic_nodes_with_sections($root); foreach my $node (@{$non_automatic_nodes}) { if (not $node->{'extra'}->{'menus'} or not scalar(@{$node->{'extra'}->{'menus'}})) { my $section = $node->{'extra'}->{'associated_section'}; my $current_menu = Texinfo::Structuring::new_complete_node_menu($node, $customization_information, $use_sections); if (defined($current_menu)) { _prepend_new_menu_in_node_section($node, $section, $current_menu); } } } } # The document is passed as customization information sub regenerate_master_menu($;$) { my $document = shift; my $use_sections = shift; my $identifier_target = $document->labels_information(); my $top_node = $identifier_target->{'Top'}; return undef if (!defined($top_node) or !$top_node->{'extra'} or !$top_node->{'extra'}->{'menus'} or !scalar(@{$top_node->{'extra'}->{'menus'}})); my $new_detailmenu = Texinfo::Structuring::new_detailmenu($document, $document->registrar(), $identifier_target, $top_node->{'extra'}->{'menus'}, $use_sections); return undef if (!defined($new_detailmenu)); my $global_detailmenu = $document->global_commands_information()->{'detailmenu'}; foreach my $menu (@{$top_node->{'extra'}->{'menus'}}) { my $detailmenu_index = 0; foreach my $entry (@{$menu->{'contents'}}) { if ($entry->{'cmdname'} and $entry->{'cmdname'} eq 'detailmenu') { # replace existing detailmenu by the master menu $new_detailmenu->{'parent'} = $menu; splice (@{$menu->{'contents'}}, $detailmenu_index, 1, $new_detailmenu); # also replace in global commands my $index = 0; my $global_detailmenu_index = -1; foreach my $detailmenu_global (@$global_detailmenu) { if ($detailmenu_global eq $entry) { $global_detailmenu_index = $index; last; } $index++; } if ($global_detailmenu_index >= 0) { splice (@$global_detailmenu, $global_detailmenu_index, 1, $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 ($detailmenu_entry->{'type'} and $detailmenu_entry->{'type'} eq 'menu_entry') { foreach my $entry_content (@{$detailmenu_entry->{'contents'}}) { if ($entry_content->{'type'} and $entry_content->{'type'} eq 'menu_entry_node') { Texinfo::Common::remove_from_array($internal_references, $entry_content); } } } } return 1; } $detailmenu_index++; } } my $last_menu = $top_node->{'extra'}->{'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 ($last_element->{'type'} and $last_element->{'type'} eq 'menu_comment' and scalar(@{$last_element->{'contents'}}) and $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 = {'type' => 'empty_line', 'text' => "\n", 'parent' => $preformatted}; push @{$preformatted->{'contents'}}, $empty_line; } } elsif ($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 = {'type' => 'menu_comment', 'parent' => $last_menu}; splice (@{$last_menu->{'contents'}}, $index, 0, $menu_comment); $index++; my $preformatted = {'type' => 'preformatted', 'parent' => $menu_comment}; push @{$menu_comment->{'contents'}}, $preformatted; my $empty_line = {'type' => 'after_menu_description_line', 'text' => "\n", 'parent' => $preformatted}; 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 ($content->{'type'} and $content->{'type'} eq 'menu_comment') { push @contents, @{$content->{'contents'}}; } elsif ($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 ($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 ($content->{'cmdname'} and $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 ($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 set_menus_to_simple_menu($) { my $nodes_list = shift; if ($nodes_list) { foreach my $node (@{$nodes_list}) { if ($node->{'extra'} and $node->{'extra'}->{'menus'}) { foreach my $menu (@{$node->{'extra'}->{'menus'}}) { menu_to_simple_menu($menu); } } } } } sub _protect_hashchar_at_line_beginning($$$) { my $type = shift; my $current = shift; my $argument = shift; my ($registrar, $customization_information) = @$argument; if ($current->{'text'} and $current->{'text'} =~ /^\s*#\s*(line)? (\d+)(( "([^"]+)")(\s+\d+)*)?\s*$/ and $current->{'parent'} and $current->{'parent'}->{'contents'}) { # find the $current element index in parent to check if first or preceded # by a new line my $parent = $current->{'parent'}; for (my $i = 0; $i < scalar(@{$parent->{'contents'}}); $i++) { if ($parent->{'contents'}->[$i] eq $current) { # protect if first in container, or if after a newline if ($i == 0 or ($parent->{'contents'}->[$i-1]->{'text'} and $parent->{'contents'}->[$i-1]->{'text'} =~ /\n$/)) { # do not actually protect in raw block command, but warn if ($current->{'type'} and $current->{'type'} eq 'raw') { my $parent_for_warn = $parent; while ($parent_for_warn) { if ($parent_for_warn->{'cmdname'} and $parent_for_warn->{'source_info'}) { if ($registrar) { $registrar->line_warn(sprintf(__( "could not protect hash character in \@%s"), $parent_for_warn->{'cmdname'}), $parent_for_warn->{'source_info'}, 0, $customization_information->get_conf('DEBUG')); } last; } $parent_for_warn = $parent_for_warn->{'parent'}; } return undef; } else { my @result = (); my $remaining_source_marks; my $current_position = 0; if ($current->{'source_marks'}) { $remaining_source_marks = [@{$current->{'source_marks'}}]; delete $current->{'source_marks'}; } $current->{'text'} =~ s/^(\s*)#//; my $e = {'text' => $1, 'parent' => $parent}; $current_position = Texinfo::Common::relocate_source_marks( $remaining_source_marks, $e, $current_position, length($1)); if ($e->{'text'} ne '' or $e->{'source_marks'}) { push @result, $e; } $e = {'cmdname' => 'hashchar', 'parent' => $parent}; my $arg = {'type' => 'brace_container', 'parent' => $e}; $e->{'args'} = [$arg]; $current_position = Texinfo::Common::relocate_source_marks( $remaining_source_marks, $e, $current_position, 1); push @result, $e; $current_position = Texinfo::Common::relocate_source_marks( $remaining_source_marks, $current, $current_position, length($current->{'text'})); push @result, $current; return \@result; } } } } } return undef; } sub protect_hashchar_at_line_beginning($;$$) { my $tree = shift; my $registrar = shift; my $customization_information = shift; return Texinfo::ManipulateTree::modify_tree($tree, \&_protect_hashchar_at_line_beginning, [$registrar, $customization_information]); } sub _protect_first_parenthesis_in_targets($$$) { my $type = shift; my $current = shift; my $argument = shift; my $element_label = Texinfo::Common::get_label_element($current); if ($element_label) { Texinfo::ManipulateTree::protect_first_parenthesis($element_label); } return undef; } # TODO not documented sub protect_first_parenthesis_in_targets($) { my $tree = shift; Texinfo::ManipulateTree::modify_tree($tree, \&_protect_first_parenthesis_in_targets); } 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($tree, $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($tree, $commands_heading_tree) X> This function adds empty C<@unnumbered> and similar commands in a tree to fill gaps in sectioning. This may be used, for example, when converting from a format that can handle gaps in sectioning. I<$tree> is the tree root, which is modified by adding the new sectioning commands. 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) =item set_menus_to_simple_menu($nodes_list) X> X> C transforms the tree of a menu tree element. C calls C for all the menus of the nodes in C<$nodes_list>. 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, $registrar, $customization_information) 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<$registrar> and I<$customization_information> arguments are optional. If defined, the I<$registrar> argument should be a L object in which the errors and warnings encountered while parsing are registered. If defined, I<$customization_information> should give access to customization through C. If both I<$registrar> and I<$customization_information> are defined they are 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, Epertusus@free.frE =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