# A style that tries to be analogous with a book, in HTML. # # Copyright 2004-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 . # # Originally written by Patrice Dumas in 2004. # # This style is based on the scriptbasic style. use strict; # To check if there is no erroneous autovivification #no autovivification qw(fetch delete exists store strict); #use Carp qw(cluck); use Texinfo::Commands; use Texinfo::Common; use Texinfo::Convert::Texinfo; # for section_level_adjusted_command_name use Texinfo::Structuring; my %sectioning_heading_commands = %Texinfo::Commands::sectioning_heading_commands; texinfo_set_from_init_file('contents', 1); texinfo_set_from_init_file('CONTENTS_OUTPUT_LOCATION', 'inline'); texinfo_set_from_init_file('NO_TOP_NODE_OUTPUT', 1); # Following Rudolf Adamkovič idea, have Contents button for regular output # units link to the section in table of contents. sub book_in_contents_button($$$) { my ($self, $direction, $element) = @_; if (exists($element->{'cmdname'}) and $element->{'cmdname'} eq 'node') { my $node_relations = $self->converter_node_relations_of_node($element); if (defined($node_relations) and exists($node_relations->{'associated_section'})) { $element = $node_relations->{'associated_section'}->{'element'}; } } my $href = $self->command_contents_href($element, 'contents'); # Call direction_string to have Contents translated. return ("[". $self->direction_string('Contents', 'text')."]", 0); } my @book_contents_buttons = ('Back', 'Forward', 'Space', 'Contents', 'Index', 'About'); foreach my $buttons ('TOP_BUTTONS') { texinfo_set_from_init_file($buttons, \@book_contents_buttons); } my @book_output_unit_buttons = ('Back', 'Forward', 'Space', ['This', \&book_in_contents_button], 'Index', 'About'); foreach my $buttons ('SECTION_BUTTONS', 'CHAPTER_BUTTONS') { texinfo_set_from_init_file($buttons, \@book_output_unit_buttons); } my @book_footer_buttons = ('Contents', 'Index', 'About'); foreach my $buttons ('MISC_BUTTONS', 'SECTION_FOOTER_BUTTONS', 'CHAPTER_FOOTER_BUTTONS', 'TOP_FOOTER_BUTTONS') { texinfo_set_from_init_file($buttons, \@book_footer_buttons); } texinfo_set_from_init_file('NODE_FOOTER_BUTTONS', ['Back', 'Forward']); texinfo_set_from_init_file('LINKS_DIRECTIONS', ['Top', 'Index', 'Contents', 'About', 'Up', 'NextFile', 'PrevFile']); texinfo_set_from_init_file('WORDS_IN_PAGE', undef); texinfo_set_from_init_file('FORMAT_MENU', 'nomenu'); texinfo_set_from_init_file('USE_NODES', 0); texinfo_set_from_init_file('BIG_RULE', '
'); my $toc_numbered_mark_class = 'toc-numbered-mark'; my ($book_previous_default_filename, $book_previous_file_name, $book_unumbered_nr); sub book_init($) { my $converter = shift; $book_previous_default_filename = undef; $book_previous_file_name = undef; $book_unumbered_nr = 0; return 0; } texinfo_register_handler('init', \&book_init); sub book_print_up_toc($$) { my ($converter, $command) = @_; my $current_relations = $converter->converter_section_relations_of_section($command); return '' if (!defined($current_relations)); my $result = ''; my @up_commands; while (exists($current_relations->{'section_directions'}) and exists($current_relations->{'section_directions'}->{'up'}) and ($current_relations->{'section_directions'}->{'up'} ne $current_relations)) { $current_relations = $current_relations->{'section_directions'}->{'up'}; unshift @up_commands, $current_relations->{'element'}; } # this happens for example for top tree unit return '' if (! scalar(@up_commands)); my $up = shift @up_commands; #print STDERR "$up ".Texinfo::Convert::Texinfo::root_heading_command_to_texinfo($up)."\n"; $result .= $converter->html_attribute_class('ul', [$toc_numbered_mark_class]).">
  • " . "command_href($up)."\">".$converter->command_text($up) . "
  • \n"; foreach my $up (@up_commands) { $result .= '
  • ' .$converter->html_attribute_class('ul', [$toc_numbered_mark_class]).">
  • " . "command_href($up)."\">".$converter->command_text($up) . "
  • \n"; } foreach my $up (@up_commands) { $result .= "\n"; } $result .= "\n"; return $result; } sub book_format_navigation_header($$$$) { my ($self, $buttons, $cmdname, $element) = @_; my $output_unit = $element->{'associated_unit'}; if (defined($output_unit) and exists($output_unit->{'unit_section'}) and exists($output_unit->{'unit_filename'}) and $self->count_elements_in_filename('current', $output_unit->{'unit_filename'}) == 1) { return book_print_up_toc($self, $output_unit->{'unit_section'}->{'element'}) . &{$self->default_formatting_function('format_navigation_header')}($self, $buttons, $cmdname, $element); } else { return &{$self->default_formatting_function('format_navigation_header')}( $self, $buttons, $cmdname, $element); } } texinfo_register_formatting_function('format_navigation_header', \&book_format_navigation_header); sub book_print_sub_toc($$); sub book_print_sub_toc($$) { my ($converter, $section_relations) = @_; my $result = ''; my $command = $section_relations->{'element'}; my $content_href = $converter->command_href($command); my $heading = $converter->command_text($command); if (defined($content_href)) { $result .= "
  • "."$heading" . "
  • \n"; } my $section_children = $section_relations->{'section_children'}; if (defined($section_children) and scalar(@{$section_children})) { $result .= '
  • '.$converter->html_attribute_class('ul', [$toc_numbered_mark_class]) .">\n". book_print_sub_toc($converter, $section_children->[0]) ."
  • \n"; } if (exists($section_relations->{'section_directions'}) and exists($section_relations->{'section_directions'}->{'next'})) { $result .= book_print_sub_toc($converter, $section_relations->{'section_directions'}->{'next'}); } return $result; } # this function is very similar with the default function, but there is # an additional sub toc before the content. It should be synced with # the default function. sub book_convert_heading_command($$$$$) { my ($self, $cmdname, $element, $args, $content) = @_; my $result = ''; # No situation where this could happen if ($self->in_string()) { $result .= $self->command_text($element, 'string') ."\n" if ($cmdname ne 'node'); $result .= $content if (defined($content)); return $result; } my $element_id = $self->command_id($element); print STDERR "CONVERT elt heading " # uncomment next line for the perl object name #."$element " .Texinfo::Convert::Texinfo::root_heading_command_to_texinfo($element)."\n" if ($self->get_conf('DEBUG')); my $document = $self->get_info('document'); my $sections_list; my $nodes_list; if (defined($document)) { $sections_list = $document->sections_list(); $nodes_list = $document->nodes_list(); } my $output_unit; my $section_relations; my $node_relations; if (exists($Texinfo::Commands::root_commands{$cmdname})) { if ($cmdname eq 'node') { if (defined($nodes_list) and exists($element->{'extra'}) and $element->{'extra'}->{'node_number'}) { $node_relations = $nodes_list->[$element->{'extra'}->{'node_number'} -1]; } } elsif (defined($sections_list)) { $section_relations = $sections_list->[$element->{'extra'}->{'section_number'} -1]; } # All the root commands are associated to an output unit, the condition # on associated_unit is always true. if (exists($element->{'associated_unit'})) { $output_unit = $element->{'associated_unit'}; } } my $element_header = ''; if ($output_unit) { $element_header = &{$self->formatting_function('format_element_header')}( $self, $cmdname, $element, $output_unit); } my $toc_or_mini_toc_or_auto_menu = ''; if ($self->get_conf('CONTENTS_OUTPUT_LOCATION') eq 'after_top' and $cmdname eq 'top' and defined($sections_list) and scalar(@{$sections_list}) > 1) { foreach my $content_command_name ('shortcontents', 'contents') { if ($self->get_conf($content_command_name)) { my $contents_text = $self->_contents_inline_element($content_command_name, undef); if ($contents_text ne '') { $toc_or_mini_toc_or_auto_menu .= $contents_text; } } } } if ($toc_or_mini_toc_or_auto_menu eq '' and $section_relations # avoid a double of contents if already after title and ($cmdname ne 'top' or $self->get_conf('CONTENTS_OUTPUT_LOCATION') ne 'after_title')) { my $section_children = $section_relations->{'section_children'}; if (defined($section_children) and scalar(@{$section_children})) { $toc_or_mini_toc_or_auto_menu .= $self->html_attribute_class('ul', [$toc_numbered_mark_class]).">\n"; $toc_or_mini_toc_or_auto_menu .= book_print_sub_toc($self, $section_children->[0]); $toc_or_mini_toc_or_auto_menu .= "\n"; } } if ($self->get_conf('NO_TOP_NODE_OUTPUT') and exists($Texinfo::Commands::root_commands{$cmdname})) { my $in_skipped_node_top = $self->get_shared_conversion_state('top', 'in_skipped_node_top'); $in_skipped_node_top = 0 if (!defined($in_skipped_node_top)); if ($in_skipped_node_top == 1) { my $id_class = $cmdname; $result .= &{$self->formatting_function('format_separate_anchor')}($self, $element_id, $id_class); $result .= $element_header; $result .= $toc_or_mini_toc_or_auto_menu; return $result; } } my $level_corrected_cmdname = $cmdname; my $level_set_class; if (exists($element->{'extra'}) and exists($element->{'extra'}->{'section_level'})) { # if the level was changed, use a consistent command name $level_corrected_cmdname = Texinfo::Structuring::section_level_adjusted_command_name($element); if ($level_corrected_cmdname ne $cmdname) { $level_set_class = "${cmdname}-level-set-${level_corrected_cmdname}"; } } # find the section starting here, can be through the associated node # preceding the section, or the section itself my $opening_section; my $level_corrected_opening_section_cmdname; if (defined($node_relations) and exists($node_relations->{'associated_section'})) { $opening_section = $node_relations->{'associated_section'}->{'element'}; $level_corrected_opening_section_cmdname = Texinfo::Structuring::section_level_adjusted_command_name( $opening_section); # if there is an associated node, it is not a section opening # the section was opened before when the node was encountered } elsif (defined($section_relations) and !exists($section_relations->{'associated_node'})) { $opening_section = $element; $level_corrected_opening_section_cmdname = $level_corrected_cmdname; } # could use empty args information also, to avoid calling command_text #my $empty_heading = (!scalar(@$args) or !defined($args->[0])); # $heading not defined may happen if the command is a @node, for example # if there is an error in the node. my $heading = $self->command_text($element); my $heading_level; # node is used as heading if there is nothing else. if (defined($node_relations)) { if (defined($output_unit) and exists($output_unit->{'unit_node'}) and $output_unit->{'unit_node'} eq $node_relations and !exists($node_relations->{'associated_title_command'})) { if ($element->{'extra'}->{'normalized'} eq 'Top') { $heading_level = 0; } else { # use node $heading_level = 3; } } } elsif (exists($element->{'extra'}) and exists($element->{'extra'}->{'section_level'})) { $heading_level = $element->{'extra'}->{'section_level'}; } else { # for *heading* @-commands which do not have a level # in the document as they are not associated with the # sectioning tree, but still have a $heading_level $heading_level = Texinfo::Common::section_level($element); } my $do_heading = (defined($heading) and $heading ne '' and defined($heading_level)); # if set, the id is associated to the heading text my $heading_id; if ($opening_section) { my $level; if (exists($opening_section->{'extra'}) and exists($opening_section->{'extra'}->{'section_level'})) { $level = $opening_section->{'extra'}->{'section_level'}; } else { # if Structuring sectioning_structure was not called on the # document (cannot happen in main program or test_utils.pl tests) $level = Texinfo::Common::section_level($opening_section); } my $closed_strings = $self->close_registered_sections_level( $self->current_filename(), $level); $result .= join('', @{$closed_strings}); $self->register_opened_section_level($self->current_filename(), $level, "\n"); # use a specific class name to mark that this is the start of # the section extent. It is not necessary where the section is. $result .= $self->html_attribute_class('div', ["${level_corrected_opening_section_cmdname}-level-extent"]); $result .= " id=\"$element_id\"" if (defined($element_id) and $element_id ne ''); $result .= ">\n"; } elsif (defined($element_id) and $element_id ne '') { if ($element_header ne '') { # case of a @node without sectioning command and with a header. # put the node element anchor before the header. # Set the class name to the command name if there is no heading, # else the class will be with the heading element. my $id_class = $cmdname; if ($do_heading) { $id_class = "${cmdname}-id"; } $result .= &{$self->formatting_function('format_separate_anchor')}($self, $element_id, $id_class); } else { $heading_id = $element_id; } } $result .= $element_header; if ($do_heading) { if ($self->get_conf('TOC_LINKS') and exists($Texinfo::Commands::root_commands{$cmdname}) and exists($sectioning_heading_commands{$cmdname})) { my $content_href = $self->command_contents_href($element, 'contents'); if (defined($content_href)) { $heading = "$heading"; } } my @heading_classes; push @heading_classes, $level_corrected_cmdname; if (defined($level_set_class)) { push @heading_classes, $level_set_class; } if ($self->in_preformatted_context()) { my $id_str = ''; if (defined($heading_id)) { $id_str = " id=\"$heading_id\""; } $result .= $self->html_attribute_class('strong', \@heading_classes) ."${id_str}>".$heading.''."\n"; } else { $result .= &{$self->formatting_function('format_heading_text')}($self, $level_corrected_cmdname, \@heading_classes, $heading, $heading_level +$self->get_conf('CHAPTER_HEADER_LEVEL') -1, $heading_id, $element, $element_id); } } elsif (defined($heading_id)) { # case of a lone node and no header, and case of an empty @top $result .= &{$self->formatting_function('format_separate_anchor')}($self, $heading_id, $cmdname); } $result .= $toc_or_mini_toc_or_auto_menu; $result .= $content if (defined($content)); return $result; } foreach my $command (keys(%Texinfo::Commands::sectioning_heading_commands), 'node') { texinfo_register_command_formatting($command, \&book_convert_heading_command); } sub book_unit_file_name($$$$) { my ($converter, $output_unit, $filename, $filepath) = @_; return (undef, undef) if (!$converter->get_conf('SPLIT')); # should only happen if ! SPLIT, so should be redundant with the # condition above return ($filename, $filepath) if (defined($filepath)); if (defined($book_previous_default_filename) and ($filename eq $book_previous_default_filename)) { return ($book_previous_file_name, undef); } my $prefix = $converter->get_info('document_name'); my $new_file_name; my $command; if (exists($output_unit->{'unit_section'})) { $command = $output_unit->{'unit_section'}->{'element'}; } return (undef, undef) unless (defined($command)); if ($converter->unit_is_top_output_unit($output_unit)) { $new_file_name = "${prefix}_top.html"; } elsif (exists($command->{'extra'}->{'section_heading_number'}) and ($command->{'extra'}->{'section_heading_number'} ne '')) { my $number = $command->{'extra'}->{'section_heading_number'}; $number .= '.' unless ($number =~ /\.$/); $new_file_name = "${prefix}_$number" . 'html'; } else { $book_unumbered_nr++; $new_file_name = "${prefix}_U." . $book_unumbered_nr . '.html'; } $book_previous_default_filename = $filename; $book_previous_file_name = $new_file_name; return ($new_file_name, undef); } texinfo_register_file_id_setting_function('unit_file_name', \&book_unit_file_name); 1;