blob: 5b66f61586dbb8f7b4ebda8f3580fdca121db157 [file] [log] [blame]
# $Id: Plaintext.pm 6991 2016-02-06 12:16:13Z gavin $
# Plaintext.pm: output tree as text with filling.
#
# Copyright 2010, 2011, 2012, 2013, 2014, 2015 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 <http://www.gnu.org/licenses/>.
#
# Original author: Patrice Dumas <pertusus@free.fr>
package Texinfo::Convert::Plaintext;
use 5.00405;
use strict;
use Texinfo::Convert::Converter;
use Texinfo::Common;
use Texinfo::Convert::Texinfo;
use Texinfo::Convert::Paragraph;
use Texinfo::Convert::Text;
use Texinfo::Convert::Line;
use Texinfo::Convert::UnFilled;
use Carp qw(cluck);
require Exporter;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
@ISA = qw(Exporter Texinfo::Convert::Converter);
# Items to export into callers namespace by default. Note: do not export
# names by default without a very good reason. Use EXPORT_OK instead.
# Do not simply export all your public functions/methods/constants.
# This allows declaration use Texinfo::Convert::Plaintext ':all';
# If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
# will save memory.
%EXPORT_TAGS = ( 'all' => [ qw(
convert
output
) ] );
@EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
@EXPORT = qw(
);
$VERSION = '6.1';
# misc commands that are of use for formatting.
my %formatting_misc_commands = %Texinfo::Convert::Text::formatting_misc_commands;
my $NO_NUMBER_FOOTNOTE_SYMBOL = '*';
my @informative_global_commands = ('paragraphindent', 'firstparagraphindent',
'frenchspacing', 'documentencoding', 'footnotestyle', 'documentlanguage',
'contents', 'shortcontents', 'summarycontents', 'setcontentsaftertitlepage',
'setshortcontentsaftertitlepage', 'deftypefnnewline');
my %informative_commands;
foreach my $informative_command (@informative_global_commands) {
$informative_commands{$informative_command} = 1;
}
my %no_brace_commands = %Texinfo::Common::no_brace_commands;
my %brace_no_arg_commands;
foreach my $command (keys (%Texinfo::Common::brace_commands)) {
$brace_no_arg_commands{$command} = 1
if ($Texinfo::Common::brace_commands{$command} == 0);
}
my %accent_commands = %Texinfo::Common::accent_commands;
my %misc_commands = %Texinfo::Common::misc_commands;
my %sectioning_commands = %Texinfo::Common::sectioning_commands;
my %def_commands = %Texinfo::Common::def_commands;
my %ref_commands = %Texinfo::Common::ref_commands;
my %block_commands = %Texinfo::Common::block_commands;
my %menu_commands = %Texinfo::Common::menu_commands;
my %root_commands = %Texinfo::Common::root_commands;
my %preformatted_commands = %Texinfo::Common::preformatted_commands;
my %explained_commands = %Texinfo::Common::explained_commands;
my %inline_format_commands = %Texinfo::Common::inline_format_commands;
my %inline_commands = %Texinfo::Common::inline_commands;
my %item_container_commands = %Texinfo::Common::item_container_commands;
my %raw_commands = %Texinfo::Common::raw_commands;
my %format_raw_commands = %Texinfo::Common::format_raw_commands;
my %code_style_commands = %Texinfo::Common::code_style_commands;
my %regular_font_style_commands = %Texinfo::Common::regular_font_style_commands;
my %preformatted_code_commands = %Texinfo::Common::preformatted_code_commands;
my %default_index_commands = %Texinfo::Common::default_index_commands;
my %letter_no_arg_commands = %Texinfo::Common::letter_no_arg_commands;
foreach my $kept_command(keys (%informative_commands),
keys (%default_index_commands),
'verbatiminclude', 'insertcopying',
'listoffloats', 'printindex', ) {
$formatting_misc_commands{$kept_command} = 1;
}
foreach my $def_command (keys(%def_commands)) {
$formatting_misc_commands{$def_command} = 1 if ($misc_commands{$def_command});
}
# There are 6 stacks that define the context.
# context: relevant for alignement of text. Set in math, footnote,
# listoffloats, flush_commands, preformatted_context_commands
# (preformatted + menu + verbatim), and raw commands if
# on top level.
# format_context: used for the count of paragraphs and for the indentation.
# Set in footnote, for all commands relevant for indenting, like
# @*table, @itemize, @enumerate, preformatted commands,
# @*quotation, @def*, and also menu commands, @flushleft,
# @flushright, @float, in multitable cell and raw commands if at
# toplevel.
# text_element_context: for the max columns and the counter in the line
# position (although the counter in the line is taken over by
# the formatter once a formatter is opened).
# Set in footnote and in multitable cells.
# formatters: a formatter environment has stacks for formatting context.
# Also holds a container, an objects that does the counting
# of columns, actual indentation. In general, it is better not
# to have formatters in parallel, but it may happen.
# count_context: holds the bytes count, the lines count and the location
# of the commands that have their byte count or lines count
# recorded. It is set for out of document formatting to avoid
# counting some converted text, but it is also set when it has
# to be modified afterwards, for aligned commands or multitable
# cells for example.
# document_context: Used to keep track if we are in a multitable.
# formatters have their own stack
# in container
# 'upper_case'
# 'var'
# 'font_type_stack'
#
# paragraph number incremented with paragraphs, center, listoffloats
# and block commands except: html and such, group, raggedright, menu*, float
my %default_preformatted_context_commands = (%preformatted_commands,
%format_raw_commands);
foreach my $preformatted_command ('verbatim', keys(%menu_commands)) {
$default_preformatted_context_commands{$preformatted_command} = 1;
}
my %ignored_misc_commands;
foreach my $misc_command (keys(%misc_commands)) {
$ignored_misc_commands{$misc_command} = 1
unless ($formatting_misc_commands{$misc_command});
}
my %ignored_commands = %ignored_misc_commands;
foreach my $ignored_brace_commands ('caption', 'shortcaption',
'hyphenation', 'sortas') {
$ignored_commands{$ignored_brace_commands} = 1;
}
my %item_indent_format_length = ('enumerate' => 2,
'itemize' => 3,
'table' => 0,
'vtable' => 0,
'ftable' => 0,
);
my $indent_length = 5;
my %indented_commands;
foreach my $indented_command (keys(%item_indent_format_length),
keys(%preformatted_commands), 'quotation', 'smallquotation',
'indentedblock', 'smallindentedblock',
keys(%def_commands)) {
$indented_commands{$indented_command} = 1
if exists($block_commands{$indented_command});
}
my %default_format_context_commands = %indented_commands;
foreach my $non_indented('format', 'smallformat') {
delete $indented_commands{$non_indented};
}
foreach my $format_context_command (keys(%menu_commands), 'verbatim',
'flushleft', 'flushright', 'multitable', 'float') {
$default_format_context_commands{$format_context_command} = 1;
}
my %flush_commands = (
'flushleft' => 1,
'flushright' => 1
);
# commands that leads to advancing the paragraph number. This is mostly
# used to determine the first line, in fact.
my %advance_paragraph_count_commands;
foreach my $command (keys(%block_commands)) {
next if ($menu_commands{$command}
or $block_commands{$command} eq 'raw');
$advance_paragraph_count_commands{$command} = 1;
}
# group and raggedright do more than not advancing para, they should also
# be transparent with respect to paragraph number counting.
foreach my $not_advancing_para ('group', 'raggedright',
'titlepage', 'copying', 'documentdescription', 'float') {
delete $advance_paragraph_count_commands{$not_advancing_para};
}
foreach my $advancing_para('center', 'verbatim', 'listoffloats') {
$advance_paragraph_count_commands{$advancing_para} = 1;
}
foreach my $ignored_block_commands ('ignore', 'macro', 'rmacro', 'copying',
'documentdescription', 'titlepage', 'direntry') {
$ignored_commands{$ignored_block_commands} = 1;
}
my %punctuation_no_arg_commands;
foreach my $punctuation_command('enddots', 'exclamdown', 'questiondown') {
$punctuation_no_arg_commands{$punctuation_command} = 1;
}
my %upper_case_commands = (
'sc' => 1,
'var' => 1
);
my %ignorable_space_types;
foreach my $type ('empty_line_after_command',
'empty_spaces_after_command', 'spaces_at_end',
'empty_spaces_before_argument', 'empty_spaces_before_paragraph',
'empty_spaces_after_close_brace',
'empty_space_at_end_def_bracketed') {
$ignorable_space_types{$type} = 1;
}
my %ignored_types;
foreach my $type ('preamble',
'preamble_before_setfilename') {
$ignored_types{$type} = 1;
}
my %ignorable_types = %ignorable_space_types;
foreach my $ignored_type(keys(%ignored_types)) {
$ignorable_types{$ignored_type} = 1;
}
# All those commands run with the text.
my %style_map = (
'strong' => '*',
'dfn' => '"',
'emph' => '_',
);
foreach my $command (keys(%style_map)) {
$style_map{$command} = [$style_map{$command}, $style_map{$command}];
}
# math is special
my @asis_commands = ('asis', 'w', 'b', 'ctrl', 'i', 'sc', 't', 'r',
'slanted', 'sansserif', 'var', 'verb', 'clicksequence',
'headitemfont', 'dmn');
foreach my $asis_command (@asis_commands) {
$style_map{$asis_command} = ['', ''];
}
my @quoted_commands = ('cite', 'code', 'command', 'env', 'file', 'kbd',
'option', 'samp', 'indicateurl');
# %non_quoted_commands_when_nested have no quote when in code command contexts
my %non_quoted_commands_when_nested;
# Quotes are reset in converter_initialize and unicode quotes are used
# if @documentencoding utf-8 is used.
foreach my $quoted_command (@quoted_commands) {
$style_map{$quoted_command} = ["'", "'"];
if ($code_style_commands{$quoted_command}) {
$non_quoted_commands_when_nested{$quoted_command} = 1;
}
}
# always quoted even when nested
delete $non_quoted_commands_when_nested{'samp'};
delete $non_quoted_commands_when_nested{'indicateurl'};
$style_map{'key'} = ['<', '>'];
$style_map{'sub'} = ['_{', '}'];
$style_map{'sup'} = ['^{', '}'];
# Commands producing styles that are output in node names and index entries.
my %index_style_commands;
for my $index_style_command ('strong', 'emph', 'sub', 'sup', 'key') {
$index_style_commands{$index_style_command} = 1;
}
# in those commands, there is no addition of double space after a dot.
# math is special
my %no_punctation_munging_commands;
foreach my $command ('var', 'cite', 'dmn', keys(%code_style_commands)) {
$no_punctation_munging_commands{$command} = 1;
}
my %defaults = (
'ENABLE_ENCODING' => 1,
'SHOW_MENU' => 0,
#'EXTENSION' => 'info',
'EXTENSION' => 'txt',
#'USE_SETFILENAME_EXTENSION' => 1,
'INFO_SPECIAL_CHARS_WARNING' => 1,
#'OUTFILE' => undef,
'OUTFILE' => '-',
'SUBDIR' => undef,
'documentlanguage' => undef,
'output_format' => '',
'USE_NODES' => 1,
);
sub push_top_formatter($$)
{
my $self = shift;
my $top_context = shift;
push @{$self->{'context'}}, $top_context;
push @{$self->{'format_context'}}, {
'cmdname' => '_top_format',
'indent_level' => 0,
'paragraph_count' => 0,
};
push @{$self->{'text_element_context'}}, {
'max' => $self->{'fillcolumn'}
};
push @{$self->{'document_context'}}, {
'in_multitable' => 0
};
# This is not really meant to be used, as contents should open
# their own formatters, however it happens that there is some text
# outside any content that needs to be formatted, as @sp for example.
push @{$self->{'formatters'}}, $self->new_formatter('line');
$self->{'formatters'}->[-1]->{'_top_formatter'} = 1;
}
my %contents_commands = (
'contents' => 1,
'shortcontents' => 1,
'summarycontents' => 1,
);
sub converter_defaults($$)
{
return %defaults;
}
sub converter_global_commands($)
{
return @informative_global_commands;
}
sub converter_initialize($)
{
my $self = shift;
$self->{'context'} = [];
$self->{'format_context'} = [];
$self->{'empty_lines_count'} = undef;
push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0,
'locations' => []};
%{$self->{'ignored_types'}} = %ignored_types;
%{$self->{'ignorable_space_types'}} = %ignorable_space_types;
%{$self->{'ignored_commands'}} = %ignored_commands;
# this is dynamic because raw formats may either be full commands if
# isolated, or simple text if in a paragraph
%{$self->{'format_context_commands'}} = %default_format_context_commands;
%{$self->{'preformatted_context_commands'}}
= %default_preformatted_context_commands;
$self->{'footnote_index'} = 0;
$self->{'pending_footnotes'} = [];
foreach my $format (keys(%format_raw_commands)) {
$self->{'ignored_commands'}->{$format} = 1
unless ($self->{'expanded_formats_hash'}->{$format});
}
%{$self->{'style_map'}} = %style_map;
if ($self->get_conf('ENABLE_ENCODING') and $self->get_conf('OUTPUT_ENCODING_NAME')
and $self->get_conf('OUTPUT_ENCODING_NAME') eq 'utf-8') {
# cache this to avoid redoing calls to get_conf
$self->{'to_utf8'} = 1;
foreach my $quoted_command (@quoted_commands) {
# Directed single quotes
$self->{'style_map'}->{$quoted_command} = ["\x{2018}", "\x{2019}"];
}
# Directed double quotes
$self->{'style_map'}->{'dfn'} = ["\x{201C}", "\x{201D}"];
}
$self->{'convert_text_options'}
= {Texinfo::Common::_convert_text_options($self)};
if (defined($self->get_conf('OPEN_QUOTE_SYMBOL'))) {
foreach my $quoted_command (@quoted_commands) {
$self->{'style_map'}->{$quoted_command}->[0]
= $self->get_conf('OPEN_QUOTE_SYMBOL');
}
}
if (defined($self->get_conf('CLOSE_QUOTE_SYMBOL'))) {
foreach my $quoted_command (@quoted_commands) {
$self->{'style_map'}->{$quoted_command}->[1]
= $self->get_conf('CLOSE_QUOTE_SYMBOL');
}
}
if ($self->get_conf('FILLCOLUMN')) {
$self->{'fillcolumn'} = $self->get_conf('FILLCOLUMN');
# else it's already set via the defaults
}
# This needs to be here to take into account $self->{'fillcolumn'}.
$self->push_top_formatter('_Root_context');
# some caching to avoid calling get_conf
if ($self->get_conf('OUTPUT_PERL_ENCODING')) {
$self->{'output_perl_encoding'} = $self->get_conf('OUTPUT_PERL_ENCODING');
} else {
$self->{'output_perl_encoding'} = '';
}
$self->{'enable_encoding'} = $self->get_conf('ENABLE_ENCODING');
$self->{'output_encoding_name'} = $self->get_conf('OUTPUT_ENCODING_NAME');
$self->{'debug'} = $self->get_conf('DEBUG');
return $self;
}
sub _count_context_bug_message($$$)
{
my $self = shift;
my $precision = shift;
my $element = shift;
if (scalar(@{$self->{'count_context'}}) != 1) {
my $element_text;
if ($element) {
$element_text
= Texinfo::Structuring::_print_element_command_texi($element);
} else {
$element_text = '';
}
$self->_bug_message("Too much count_context ${precision}(".
scalar(@{$self->{'count_context'}}). "): ". $element_text, $element);
die;
}
}
sub _convert_element($$)
{
my $self = shift;
my $element = shift;
my $result = '';
print STDERR "NEW NODE\n" if ($self->{'debug'});
$result .= $self->_convert($element);
$self->_count_context_bug_message('', $element);
print STDERR "END NODE ($self->{'count_context'}->[-1]->{'lines'},$self->{'count_context'}->[-1]->{'bytes'})\n" if ($self->{'debug'});
$result .= $self->_footnotes($element);
$self->_count_context_bug_message('footnotes ', $element);
print STDERR "AFTER FOOTNOTES ($self->{'count_context'}->[-1]->{'lines'},$self->{'count_context'}->[-1]->{'bytes'})\n" if ($self->{'debug'});
return $result;
}
sub convert($$)
{
my $self = shift;
my $root = shift;
my $result = '';
my $elements = Texinfo::Structuring::split_by_node($root);
$self->{'empty_lines_count'} = 1;
if (!defined($elements)) {
$result = $self->_convert($root);
$self->_count_context_bug_message('no element ');
my $footnotes = $self->_footnotes();
$self->_count_context_bug_message('no element footnotes ');
$result .= $footnotes;
} else {
foreach my $node (@$elements) {
my $node_text = _convert_element($self, $node);
$result .= $node_text;
}
}
return $result;
}
sub convert_tree($$)
{
my $self = shift;
my $root = shift;
$self->{'empty_lines_count'} = 1;
my $result;
if ($root->{'type'} and $root->{'type'} eq 'element') {
$result = _convert_element($self, $root);
} else {
$result = $self->_convert($root);
}
return $result;
}
# old implementation of output that does not allow for splitting.
sub _output_old($$)
{
my $self = shift;
my $root = shift;
my $outfile = '-';
$self->set_conf('OUTFILE', '-');
$self->_set_outfile();
if (defined($self->{'output_file'})) {
$outfile = $self->{'output_file'};
}
my $fh;
if ($outfile ne '') {
if ($self->get_conf('VERBOSE')) {
print STDERR "Output file $outfile\n";
}
$fh = $self->Texinfo::Common::open_out($outfile);
if (!$fh) {
$self->document_error(sprintf($self->__("could not open %s for writing: %s"),
$outfile, $!));
return undef;
}
}
my $result = $self->convert($root);
if (defined($result)) {
if (defined($fh)) {
print $fh $result;
} else {
return $result;
}
}
return undef;
}
my $end_sentence = quotemeta('.?!');
my $after_punctuation = quotemeta('"\')]');
sub _protect_sentence_ends ($) {
my $text = shift;
# Avoid suppressing end of sentence, by inserting a control character
# in front of the full stop. The choice of BS for this is arbitrary.
$text =~ s/(?<=[^[:upper:]])
(?=[$end_sentence][$after_punctuation]*(?:\s|$))
/\x08/gx;
# Also insert a control character at end of string, to protect a full stop
# that may follow later.
#$text =~ s/(?<=[^[:upper:]][$after_punctuation]*)$/\x08/;
# Perl doesn't support "variable length lookbehind"
$text = reverse $text;
$text =~ s/^(?=[$after_punctuation]*
(?:[^[:upper:]\s]|[\x{202f}\x{00a0}]))
/\x08/x;
$text = reverse $text;
return $text;
}
# Convert ``, '', `, ', ---, -- in $COMMAND->{'text'} to their output,
# possibly coverting to upper case as well.
sub _process_text($$$)
{
my $self = shift;
my $command = shift;
my $context = shift;
my $text = $command->{'text'};
if ($context->{'upper_case'}
or $self->{'formatters'}[-1]->{'var'}) {
$text = _protect_sentence_ends($text);
if ($self->{'debug'}) {
my $debug_text = $text;
$debug_text =~ s/\x08/!!/g;
print STDERR "markers:<$debug_text>\n";
}
$text = uc($text);
}
if ($self->{'to_utf8'}) {
return Texinfo::Convert::Unicode::unicode_text($text,
$context->{'font_type_stack'}->[-1]->{'monospace'});
} elsif (!$context->{'font_type_stack'}->[-1]->{'monospace'}) {
$text =~ s/---/\x{1F}/g;
$text =~ s/--/-/g;
$text =~ s/\x{1F}/--/g;
$text =~ s/``/"/g;
$text =~ s/\'\'/"/g;
$text =~ s/`/'/g;
}
return $text;
}
sub new_formatter($$;$)
{
my $self = shift;
my $type = shift;
my $conf = shift;
my $first_indent_length = $conf->{'first_indent_length'};
delete $conf->{'first_indent_length'};
my $container;
my $container_conf = {
'max' => $self->{'text_element_context'}->[-1]->{'max'},
'indent_level' => $self->{'format_context'}->[-1]->{'indent_level'},
};
$container_conf->{'frenchspacing'} = 1
if ($self->{'conf'}->{'frenchspacing'} eq 'on');
$container_conf->{'counter'}
= $self->{'text_element_context'}->[-1]->{'counter'}
if (defined($self->{'text_element_context'}->[-1]->{'counter'}));
$container_conf->{'DEBUG'} = 1 if ($self->{'debug'});
if ($conf) {
foreach my $key (keys(%$conf)) {
$container_conf->{$key} = $conf->{$key};
}
}
my $indent = $container_conf->{'indent_length'};
$indent = $indent_length*$container_conf->{'indent_level'}
if (!defined($indent));
if ($first_indent_length) {
$container_conf->{'indent_length'} = $first_indent_length;
$container_conf->{'indent_length_next'} = $indent;
} else {
$container_conf->{'indent_length'} = $indent;
}
if ($type eq 'line') {
$container = Texinfo::Convert::Line->new($container_conf);
} elsif ($type eq 'paragraph') {
$container = Texinfo::Convert::Paragraph->new($container_conf);
} elsif ($type eq 'unfilled') {
$container = Texinfo::Convert::UnFilled->new($container_conf);
} else {
die "Unknown container type $type\n";
}
if ($flush_commands{$self->{'context'}->[-1]}) {
$container->set_space_protection(undef, 1, 1);
}
my $formatter = {'container' => $container, 'upper_case' => 0,
#'code' => 0, 'code_command'=> 0,
'font_type_stack' => [{}],
'w' => 0, 'type' => $type,
'frenchspacing_stack' => [$self->{'conf'}->{'frenchspacing'}],
'suppress_styles' => $conf->{'suppress_styles'}};
if ($type eq 'unfilled') {
foreach my $context (reverse(@{$self->{'context'}})) {
if ($menu_commands{$context}) {
last;
} elsif ($preformatted_code_commands{$context}
or $format_raw_commands{$context}) {
$formatter->{'font_type_stack'}->[-1]->{'monospace'} = 1;
$formatter->{'font_type_stack'}->[-1]->{'code_command'} = 1
if ($preformatted_code_commands{$context});
last;
}
}
}
print STDERR "NEW FORMATTER($type)\n" if ($self->{'debug'});
return $formatter;
}
sub convert_line($$;$)
{
my $self = shift;
my $converted = shift;
my $conf = shift;
my $formatter = $self->new_formatter('line', $conf);
push @{$self->{'formatters'}}, $formatter;
my $text = $self->_convert($converted);
$text .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->end());
pop @{$self->{'formatters'}};
return $text;
}
sub convert_unfilled($$;$)
{
my $self = shift;
my $converted = shift;
my $conf = shift;
my $formatter = $self->new_formatter('unfilled', $conf);
#$formatter->{'code'} = 1;
$formatter->{'font_type_stack'}->[-1]->{'monospace'} = 1;
push @{$self->{'formatters'}}, $formatter;
my $result = $self->_convert($converted);
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->end());
pop @{$self->{'formatters'}};
return $result;
}
sub count_bytes($$)
{
my $self = shift;
my $string = shift;
return Texinfo::Common::count_bytes($self, $string,
$self->{'output_perl_encoding'});
}
sub _add_text_count($$)
{
my $self = shift;
my $text = shift;
if (!$self->{'count_context'}->[-1]->{'pending_text'}) {
$self->{'count_context'}->[-1]->{'pending_text'} = '';
}
$self->{'count_context'}->[-1]->{'pending_text'} .= $text;
}
sub _add_lines_count($$)
{
my $self = shift;
my $lines_count = shift;
$self->{'count_context'}->[-1]->{'lines'} += $lines_count;
}
# Update $SELF->{'count_context'}->[-1]->{'bytes'} by counting the text that
# hasn't been counted yet. It is faster to count the text all together than
# piece by piece in _add_text_count.
sub _update_count_context($)
{
my $self = shift;
if ($self->{'count_context'}->[-1]->{'pending_text'}) {
$self->{'count_context'}->[-1]->{'bytes'} +=
Texinfo::Common::count_bytes($self,
$self->{'count_context'}->[-1]->{'pending_text'},
$self->{'output_perl_encoding'});
$self->{'count_context'}->[-1]->{'pending_text'} = '';
}
}
# Save the line and byte offset of $ROOT.
sub _add_location($$)
{
my $self = shift;
my $root = shift;
my $location = { 'lines' => $self->{'count_context'}->[-1]->{'lines'} };
push @{$self->{'count_context'}->[-1]->{'locations'}}, $location;
if (!($root->{'extra'} and $root->{'extra'}->{'index_entry'})) {
_update_count_context($self);
$location->{'bytes'} = $self->{'count_context'}->[-1]->{'bytes'};
$location->{'root'} = $root;
} else {
$location->{'index_entry'} = $root;
}
return $location;
}
sub _add_image($$$$;$)
{
my $self = shift;
my $root = shift;
my $lines_count = shift;
my $image_width = shift;
my $no_align = shift;
push @{$self->{'count_context'}->[-1]->{'images'}}, {
'lines' => $self->{'count_context'}->[-1]->{'lines'},
'lines_count' => $lines_count,
'image_width' => $image_width,
'no_align' => $no_align,
# may be used for debugging?
#'_ref' => $root,
};
}
sub _count_added($$$)
{
my $self = shift;
my $container = shift;
my $text = shift;
$self->_add_lines_count($container->end_line_count());
#$self->_add_text_count($text);
#$self->{'count_context'}->[-1]->{'bytes'} +=
# Texinfo::Common::count_bytes($self, $text,
# $self->{'output_perl_encoding'});
if (!defined $self->{'count_context'}->[-1]->{'pending_text'}) {
$self->{'count_context'}->[-1]->{'pending_text'} = '';
}
$self->{'count_context'}->[-1]->{'pending_text'} .= $text;
return $text;
}
sub _update_locations_counts($$)
{
my $self = shift;
my $locations = shift;
_update_count_context($self);
foreach my $location (@$locations) {
$location->{'bytes'} += $self->{'count_context'}->[-1]->{'bytes'}
if (defined($location->{'bytes'}));
$location->{'lines'} += $self->{'count_context'}->[-1]->{'lines'}
if (defined($location->{'lines'}));
}
}
sub _add_newline_if_needed($) {
my $self = shift;
if (defined($self->{'empty_lines_count'})
and $self->{'empty_lines_count'} == 0) {
_add_text_count($self, "\n");
_add_lines_count($self, 1);
$self->{'empty_lines_count'} = 1;
return "\n";
}
return '';
}
my $footnote_indent = 3;
sub _footnotes($;$)
{
my $self = shift;
my $element = shift;
$element = undef if ($element and $element->{'extra'}->{'no_node'});
my $result = '';
if (scalar(@{$self->{'pending_footnotes'}})) {
$result .= _add_newline_if_needed($self);
print STDERR "FOOTNOTES ".scalar(@{$self->{'pending_footnotes'}})."\n"
if ($self->{'debug'});
if ($self->get_conf('footnotestyle') eq 'end' or !defined($element)) {
my $footnotes_header = " ---------- Footnotes ----------\n\n";
$result .= $footnotes_header;
_add_text_count($self, $footnotes_header);
_add_lines_count($self, 2);
$self->{'empty_lines_count'} = 1;
} else {
my $node_contents = [@{$element->{'extra'}->{'node'}->{'extra'}->{'node_content'}},
{'text' => '-Footnotes'}];
my $normalized
= Texinfo::Convert::NodeNameNormalization::normalize_node(
{'contents' => $node_contents});
my $footnotes_node = {
'cmdname' => 'node',
'node_up' => $element->{'extra'}->{'node'},
'extra' => {'node_content' => $node_contents,
'normalized' => $normalized}
};
$result .= $self->_node($footnotes_node);
$self->{'node'} = $footnotes_node;
}
while (@{$self->{'pending_footnotes'}}) {
my $footnote = shift (@{$self->{'pending_footnotes'}});
# If nested within another footnote and footnotestyle is separate,
# the element here will be the parent element and not the footnote
# element, while the pxref will point to the name with the
# footnote node taken into account. Not really problematic as
# nested footnotes are not right.
if ($element) {
my $node_contents = [@{$element->{'extra'}->{'node'}->{'extra'}->{'node_content'}},
{'text' => "-Footnote-$footnote->{'number'}"}];
my $normalized
= Texinfo::Convert::NodeNameNormalization::normalize_node({'contents' => $node_contents});
$self->_add_location({'cmdname' => 'anchor',
'extra' => {'node_content' => $node_contents,
'normalized' => $normalized}
});
}
# this pushes on 'context', 'formatters', 'format_context',
# 'text_element_context' and 'document_context'
$self->push_top_formatter('footnote');
my $formatted_footnote_number;
if ($self->get_conf('NUMBER_FOOTNOTES')) {
$formatted_footnote_number = $footnote->{'number'};
} else {
$formatted_footnote_number = $NO_NUMBER_FOOTNOTE_SYMBOL;
}
my $footnote_text = ' ' x $footnote_indent
. "($formatted_footnote_number) ";
$result .= $footnote_text;
$self->{'text_element_context'}->[-1]->{'counter'} +=
Texinfo::Convert::Unicode::string_width($footnote_text);
_add_text_count($self, $footnote_text);
$self->{'empty_lines_count'} = 0;
$result .= $self->_convert($footnote->{'root'}->{'args'}->[0]);
$result .= _add_newline_if_needed($self);
my $old_context = pop @{$self->{'context'}};
die if ($old_context ne 'footnote');
pop @{$self->{'formatters'}};
pop @{$self->{'format_context'}};
pop @{$self->{'text_element_context'}};
pop @{$self->{'document_context'}};
}
}
$self->{'footnote_index'} = 0;
return $result;
}
sub _compute_spaces_align_line($$$;$)
{
my $line_width = shift;
my $max_column = shift;
my $direction = shift;
my $no_align = shift;
my $spaces_prepended;
if ($line_width > $max_column or $no_align) {
$spaces_prepended = 0;
} elsif ($direction eq 'center') {
# if no int we may end up with floats...
$spaces_prepended = int(($max_column -1 - $line_width) /2);
} else {
$spaces_prepended = ($max_column -1 - $line_width);
}
return $spaces_prepended;
}
sub _align_lines($$$$$$)
{
my $self = shift;
my $text = shift;
my $max_column = shift;
my $direction = shift;
my $locations = shift;
my $images = shift;
my $result = '';
my $updated_locations = {};
if ($locations and @$locations) {
foreach my $location (@$locations) {
next unless (defined($location->{'bytes'}));
#print STDERR "L anchor $location->{'root'}->{'extra'}->{'normalized'}: $location->{'lines'} ($location->{'bytes'})\n";
push @{$updated_locations->{$location->{'lines'}}}, $location;
}
}
my $images_marks = {};
if ($images and @$images) {
foreach my $image (@$images) {
#print STDERR "I $image->{'lines'}, $image->{'lines_count'}, $image->{'image_width'}\n";
if ($image->{'lines_count'} > 1) {
if (!$images_marks->{$image->{'lines'}}) {
$images_marks->{$image->{'lines'}} = $image;
}# else {
# Happens in Info with the special construct as, in that
# case, there are no lines! So no error...
# $self->_bug_message("more than one image with lines on $image->{'lines'}");
# in that case, the $image->{'lines'} is not in sync with the
# lines count. So the second image will be treated as simple text.
#}
}
}
}
my $bytes_count = 0;
my $delta_bytes = 0;
my $line_index = 0;
my $image;
my $image_lines_count;
my $image_prepended_spaces;
foreach my $line (split /^/, $text) {
my $line_bytes_begin = 0;
my $line_bytes_end = 0;
my $removed_line_bytes_end = 0;
my $removed_line_bytes_begin = 0;
my ($new_image, $new_image_prepended_spaces);
if ($images_marks->{$line_index}) {
$new_image = $images_marks->{$line_index};
$image_lines_count = 0;
$new_image_prepended_spaces
= _compute_spaces_align_line($new_image->{'image_width'}, $max_column,
$direction, $new_image->{'no_align'});
if (!defined($image)) {
$image = $new_image;
$image_prepended_spaces = $new_image_prepended_spaces;
$new_image = undef;
}
}
my $orig_line;
if (!$image) {
my $chomped = chomp($line);
# for debugging.
$orig_line = $line;
$removed_line_bytes_end -= $self->count_bytes($chomped);
#$line_bytes_end -= $self->count_bytes($chomped);
$line =~ s/^(\s*)//;
$removed_line_bytes_begin -= $self->count_bytes($1);
#$line_bytes_begin -= $self->count_bytes($1);
$line =~ s/(\s*)$//;
$removed_line_bytes_end -= $self->count_bytes($1);
#$line_bytes_end -= $self->count_bytes($1);
my $line_width = Texinfo::Convert::Unicode::string_width($line);
if ($line_width == 0) {
$result .= "\n";
$line_bytes_end += $self->count_bytes("\n");
$bytes_count += $self->count_bytes("\n");
} else {
my $spaces_prepended
= _compute_spaces_align_line($line_width, $max_column, $direction);
$result .= ' ' x$spaces_prepended . $line ."\n";
$line_bytes_begin += $self->count_bytes(' ' x$spaces_prepended);
$line_bytes_end += $self->count_bytes("\n");
$bytes_count += $line_bytes_begin + $line_bytes_end
+ $self->count_bytes($line);
}
} else {
$image_lines_count++;
my $prepended_spaces = $image_prepended_spaces;
# adjust if there is something else that the image on the first or
# last line. The adjustment is approximate.
if (($image_lines_count == 1 or $image_lines_count == $image->{'lines_count'})
and Texinfo::Convert::Unicode::string_width($line) > $image->{'image_width'}) {
$prepended_spaces
-= Texinfo::Convert::Unicode::string_width($line) - $image->{'image_width'};
$prepended_spaces = 0 if ($prepended_spaces < 0);
}
$result .= ' ' x$prepended_spaces . $line;
$line_bytes_begin += $self->count_bytes(' ' x$prepended_spaces);
$bytes_count += $line_bytes_begin + $self->count_bytes($line);
if ($new_image) {
$image = $new_image;
$image_prepended_spaces = $new_image_prepended_spaces;
} elsif ($image_lines_count == $image->{'lines_count'}) {
$image = undef;
$image_lines_count = undef;
$image_prepended_spaces = undef;
}
}
if ($updated_locations->{$line_index}) {
foreach my $location (@{$updated_locations->{$line_index}}) {
$location->{'bytes'} += $line_bytes_begin + $removed_line_bytes_begin
+ $delta_bytes;
#print STDERR "UPDATE ALIGN: $location->{'root'}->{'extra'}->{'normalized'}: ($location->{'bytes'})\n";
}
}
$delta_bytes += $line_bytes_begin + $line_bytes_end
+ $removed_line_bytes_begin + $removed_line_bytes_end;
#print STDERR "ALIGN $orig_line ($line_index. lbb $line_bytes_begin, lbe $line_bytes_end, rlbb $removed_line_bytes_begin, rlbe $removed_line_bytes_end delta $delta_bytes, bytes_count $bytes_count)\n";
$line_index++;
}
return ($result, $bytes_count);
}
sub _align_environment($$$$)
{
my $self = shift;
my $result = shift;
my $max = shift;
my $align = shift;
_update_count_context($self);
my $counts = pop @{$self->{'count_context'}};
my $bytes_count;
($result, $bytes_count) = $self->_align_lines($result, $max,
$align, $counts->{'locations'}, $counts->{'images'});
$self->_update_locations_counts($counts->{'locations'});
$self->{'count_context'}->[-1]->{'bytes'} += $bytes_count;
$self->{'count_context'}->[-1]->{'lines'} += $counts->{'lines'};
push @{$self->{'count_context'}->[-1]->{'locations'}},
@{$counts->{'locations'}};
return $result;
}
sub _contents($$$)
{
my $self = shift;
my $section_root = shift;
my $contents_or_shortcontents = shift;
my $contents = 1 if ($contents_or_shortcontents eq 'contents');
# no sections
return ('', 0) if (!$section_root or !$section_root->{'section_childs'});
my $root_level = $section_root->{'section_childs'}->[0]->{'level'};
foreach my $top_section(@{$section_root->{'section_childs'}}) {
$root_level = $top_section->{'level'}
if ($top_section->{'level'} < $root_level);
}
my $result = '';
my $lines_count = 0;
# This is done like that because the tree may not be well formed if
# there is a @part after a @chapter for example.
foreach my $top_section (@{$section_root->{'section_childs'}}) {
my $section = $top_section;
SECTION:
while ($section) {
push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0};
my $section_title_tree;
if (defined($section->{'number'})
and ($self->get_conf('NUMBER_SECTIONS')
or !defined($self->get_conf('NUMBER_SECTIONS')))) {
if ($section->{'cmdname'} eq 'appendix' and $section->{'level'} == 1) {
$section_title_tree = $self->gdt('Appendix {number} {section_title}',
{'number' => {'text' => $section->{'number'}},
'section_title'
=> {'contents' => $section->{'extra'}->{'misc_content'}}});
} else {
$section_title_tree = $self->gdt('{number} {section_title}',
{'number' => {'text' => $section->{'number'}},
'section_title'
=> {'contents' => $section->{'extra'}->{'misc_content'}}});
}
} else {
$section_title_tree = {'contents' => $section->{'extra'}->{'misc_content'}};
}
my $section_title = $self->convert_line(
{'contents' => [$section_title_tree],
'type' => 'frenchspacing'});
pop @{$self->{'count_context'}};
my $text = $section_title;
chomp ($text);
$text .= "\n";
my $repeat_count = 2 * ($section->{'level'} - ($root_level+1));
($result .= (' ' x $repeat_count)) if $repeat_count > 0;
$result .= $text;
$lines_count++;
if ($section->{'section_childs'}
and ($contents or $section->{'level'} < $root_level+1)) {
$section = $section->{'section_childs'}->[0];
} elsif ($section->{'section_next'}) {
last if ($section eq $top_section);
$section = $section->{'section_next'};
} else {
last if ($section eq $top_section);
while ($section->{'section_up'}) {
$section = $section->{'section_up'};
last SECTION if ($section eq $top_section);
if ($section->{'section_next'}) {
$section = $section->{'section_next'};
last;
}
}
}
}
}
return ($result, $lines_count);
}
sub _menu($$)
{
my $self = shift;
my $menu_command = shift;
if ($menu_command->{'cmdname'} eq 'menu') {
my $result = "* Menu:\n\n";
_add_text_count($self, $result);
_add_lines_count($self, 2);
return $result;
} else {
return '';
}
}
sub _printindex($$)
{
my $self = shift;
my $printindex = shift;
return $self->_printindex_formatted($printindex);
}
sub _normalize_top_node($)
{
my $node = shift;
return Texinfo::Common::normalize_top_node_name($node);
}
# convert and cache a node name. $NODE is a node element.
sub _node_line($$)
{
my $self = shift;
my $node = shift;
if (!$self->{'node_lines_text'}->{$node}) {
my $node_text = {'type' => '_code',
'contents' => $node->{'extra'}->{'node_content'}};
push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0};
$self->{'node_lines_text'}->{$node}->{'text'}
= _normalize_top_node($self->convert_line($node_text,
{'suppress_styles' => 1}));
_update_count_context($self);
my $end_context = pop @{$self->{'count_context'}};
$self->{'node_lines_text'}->{$node}->{'count'}
= $end_context->{'bytes'};
}
return ($self->{'node_lines_text'}->{$node}->{'text'},
$self->{'node_lines_text'}->{$node}->{'count'});
}
my $index_length_to_node = 41;
sub _printindex_formatted($$;$)
{
my $self = shift;
my $printindex = shift;
my $in_info = shift;
my $index_name;
if ($printindex->{'extra'} and $printindex->{'extra'}->{'misc_args'}
and defined($printindex->{'extra'}->{'misc_args'}->[0])) {
$index_name = $printindex->{'extra'}->{'misc_args'}->[0];
} else {
return '';
}
# this is not redone for each index, only once
if (!defined($self->{'index_entries'}) and $self->{'parser'}) {
my ($index_names, $merged_indices)
= $self->{'parser'}->indices_information();
my $merged_index_entries
= Texinfo::Structuring::merge_indices($index_names);
$self->{'index_entries'}
= $self->Texinfo::Structuring::sort_indices($merged_index_entries,
$index_names);
$self->{'index_names'} = $index_names;
}
if (!$self->{'index_entries'} or !$self->{'index_entries'}->{$index_name}
or ! @{$self->{'index_entries'}->{$index_name}}) {
return '';
}
my $result = '';
$result .= _add_newline_if_needed($self);
if ($in_info) {
my $info_printindex_magic = "\x{00}\x{08}[index\x{00}\x{08}]\n";
$result .= $info_printindex_magic;
_add_text_count($self, $info_printindex_magic);
_add_lines_count($self, 1);
}
my $heading = "* Menu:\n\n";
$result .= $heading;
_add_text_count($self, $heading);
_add_lines_count($self, 2);
# first determine the line numbers for the spacing of their formatting
my %line_nrs;
my %entry_nodes;
my $max_index_line_nr_string_length = 0;
my %ignored_entries;
foreach my $entry (@{$self->{'index_entries'}->{$index_name}}) {
my $line_nr;
if (defined ($self->{'index_entries_line_location'}->{$entry->{'command'}})) {
$line_nr = $self->{'index_entries_line_location'}->{$entry->{'command'}}->{'lines'};
# ignore index entries in special regions that haven't been seen
} elsif ($entry->{'region'}) {
$ignored_entries{$entry} = 1;
next;
}
my $node;
# priority given to the location determined dynamically as the
# index entry may be in footnote.
if (defined($self->{'index_entries_line_location'}->{$entry->{'command'}}->{'node'})) {
$node = $self->{'index_entries_line_location'}->{$entry->{'command'}}->{'node'};
} elsif (defined($entry->{'node'})) {
$node = $entry->{'node'};
}
$entry_nodes{$entry} = $node;
if (!defined($node)) {
$line_nr = 0;
} elsif($in_info) {
$line_nr = 3 if (defined($line_nr) and $line_nr < 3);
$line_nr = 4 if (!defined($line_nr));
} else {
$line_nr = 0 if (!defined($line_nr));
}
my $index_line_nr_string_length =
Texinfo::Convert::Unicode::string_width($line_nr);
$max_index_line_nr_string_length = $index_line_nr_string_length
if ($max_index_line_nr_string_length < $index_line_nr_string_length);
$line_nrs{$entry} = $line_nr;
}
# this is used to count entries that are the same
my %entry_counts = ();
foreach my $entry (@{$self->{'index_entries'}->{$index_name}}) {
#my @keys = keys(%$entry);
#print STDERR "$index_name $entry: @keys\n";
next if ($ignored_entries{$entry});
my $entry_tree = {'contents' => $entry->{'content'}};
if ($entry->{'in_code'}) {
$entry_tree->{'type'} = '_code';
} else {
$entry_tree->{'type'} = 'frenchspacing';
}
my $entry_text = '';
$entry_text .= $self->convert_line($entry_tree, {'indent' => 0,
'suppress_styles' => 1});
next if ($entry_text !~ /\S/);
# FIXME protect instead
if ($entry_text =~ /:/ and $self->get_conf('INDEX_SPECIAL_CHARS_WARNING')) {
$self->line_warn (sprintf($self->__("Index entry in \@%s with : produces invalid Info: %s"),
$entry->{'index_at_command'},
Texinfo::Convert::Texinfo::convert($entry_tree)),
$entry->{'command'}->{'line_nr'});
}
my $entry_nr = '';
if (!defined($entry_counts{$entry_text})) {
$entry_counts{$entry_text} = 0;
} else {
$entry_counts{$entry_text}++;
$entry_nr = ' <'.$entry_counts{$entry_text}.'>';
_add_text_count($self, $entry_nr);
}
my $entry_line = "* $entry_text${entry_nr}: ";
_add_text_count($self, "* ".": ");
#_add_text_count($self, $entry_line);
my $line_width = Texinfo::Convert::Unicode::string_width($entry_line);
my $entry_line_addition = '';
if ($line_width < $index_length_to_node) {
my $spaces = ' ' x ($index_length_to_node - $line_width);
$entry_line_addition .= $spaces;
_add_text_count($self, $spaces);
}
my $node = $entry_nodes{$entry};
if (!defined($node)) {
# cache the transformation to text and byte counting, as
# it is likeky that there is more than one such entry
if (!$self->{'outside_of_any_node_text'}) {
push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0};
my $node_text = $self->gdt('(outside of any node)');
$self->{'outside_of_any_node_text'}->{'text'}
= $self->convert_line($node_text);
_update_count_context($self);
my $end_context = pop @{$self->{'count_context'}};
$self->{'outside_of_any_node_text'}->{'count'}
= $end_context->{'bytes'};
}
$entry_line_addition .= $self->{'outside_of_any_node_text'}->{'text'};
$self->{'count_context'}->[-1]->{'bytes'}
+= $self->{'outside_of_any_node_text'}->{'count'};
# FIXME when outside of sectioning commands this message was already
# done by the Parser.
# Warn, only once.
if (!$self->{'index_entries_no_node'}->{$entry}) {
$self->line_warn(sprintf($self->__("entry for index `%s' outside of any node"),
$index_name), $entry->{'command'}->{'line_nr'});
$self->{'index_entries_no_node'}->{$entry} = 1;
}
} else {
my ($node_line, $byte_count) = $self->_node_line($node);
$entry_line_addition .= $node_line;
$self->{'count_context'}->[-1]->{'bytes'} += $byte_count;
}
$entry_line_addition .= '.';
_add_text_count($self, '.');
$entry_line .= $entry_line_addition;
$result .= $entry_line;
my $line_nr = $line_nrs{$entry};
my $line_nr_spaces = sprintf("%${max_index_line_nr_string_length}d", $line_nr);
my $line_part = "(line ${line_nr_spaces})";
$line_width += Texinfo::Convert::Unicode::string_width($entry_line_addition);
my $line_part_width = Texinfo::Convert::Unicode::string_width($line_part);
if ($line_width + $line_part_width +1 > $self->{'fillcolumn'}) {
$line_part = "\n" . ' ' x ($self->{'fillcolumn'} - $line_part_width)
. "$line_part\n";
_add_lines_count($self, 1);
} else {
$line_part
= ' ' x ($self->{'fillcolumn'} - $line_part_width - $line_width)
. "$line_part\n";
}
_add_lines_count($self, 1);
_add_text_count($self, $line_part);
$result .= $line_part;
}
$result .= "\n";
_add_text_count($self, "\n");
_add_lines_count($self, 1);
return $result;
}
sub _node($$)
{
my $self = shift;
my $node = shift;
return '';
}
# no error in plaintext
sub _error_outside_of_any_node($$)
{
my $self = shift;
my $root = shift;
}
sub _anchor($$)
{
my $self = shift;
my $anchor = shift;
if (!($self->{'multiple_pass'} or $self->{'in_copying_header'})) {
$self->_add_location($anchor);
$self->_error_outside_of_any_node($anchor);
}
return '';
}
my $listoffloat_entry_length = 41;
my $listoffloat_append = '...';
sub ensure_end_of_line($$)
{
my $self = shift;
my $text = shift;
my $chomped = chomp ($text);
if ($chomped) {
$self->{'count_context'}->[-1]->{'bytes'} -= $self->count_bytes($chomped);
$self->{'count_context'}->[-1]->{'lines'} -= 1;
}
$text .= "\n";
_add_text_count($self, "\n");
_add_lines_count($self, 1);
return $text;
}
sub _image_text($$$)
{
my $self = shift;
my $root = shift;
my $basefile = shift;
my $txt_file = $self->Texinfo::Common::locate_include_file($basefile.'.txt');
if (!defined($txt_file)) {
return undef;
} else {
my $filehandle = do { local *FH };
if (open ($filehandle, $txt_file)) {
binmode($filehandle, ":encoding("
.$self->get_conf('INPUT_PERL_ENCODING').")")
if (defined($self->get_conf('INPUT_PERL_ENCODING')));
my $result = '';
my $max_width = 0;
while (<$filehandle>) {
my $width = Texinfo::Convert::Unicode::string_width($_);
if ($width > $max_width) {
$max_width = $width;
}
$result .= $_;
}
# remove last end of line
chomp ($result);
if (!close ($filehandle)) {
$self->document_warn(sprintf($self->__("error on closing image text file %s: %s"),
$txt_file, $!));
}
return ($result, $max_width);
} else {
$self->line_warn(sprintf($self->__("\@image file `%s' unreadable: %s"),
$txt_file, $!), $root->{'line_nr'});
}
}
return undef;
}
sub _image_formatted_text($$$$)
{
my $self = shift;
my $root = shift;
my $basefile = shift;
my $text = shift;
my $result;
if (defined($text)) {
$result = $text;
} elsif (defined($root->{'extra'}->{'brace_command_contents'}->[3])) {
$result = '[' .Texinfo::Convert::Text::convert(
{'contents' => $root->{'extra'}->{'brace_command_contents'}->[3]},
$self->{'convert_text_options'}) .']';
} else {
$self->line_warn(sprintf($self->__(
"could not find \@image file `%s.txt' nor alternate text"),
$basefile), $root->{'line_nr'});
$result = '['.$basefile.']';
}
return $result;
}
sub _image($$)
{
my $self = shift;
my $root = shift;
if (defined($root->{'extra'}->{'brace_command_contents'}->[0])) {
my $basefile = Texinfo::Convert::Text::convert(
{'contents' => $root->{'extra'}->{'brace_command_contents'}->[0]},
{'code' => 1, %{$self->{'convert_text_options'}}});
my ($text, $width) = $self->_image_text($root, $basefile);
my $result = $self->_image_formatted_text($root, $basefile, $text);
my $lines_count = ($result =~ tr/\n/\n/);
if (!defined($width)) {
$width = Texinfo::Convert::Unicode::string_width($result);
}
# the last line is part of the image but do not have a new line,
# so 1 is added to $lines_count to have the number of lines of
# the image
$self->_add_image($root, $lines_count+1, $width);
return ($result, $lines_count);
}
return ('', 0);
}
sub _get_form_feeds($)
{
my $form_feeds = shift;
$form_feeds =~ s/^[^\f]*//;
$form_feeds =~ s/[^\f]$//;
return $form_feeds;
}
sub _convert($$);
# Convert the Texinfo tree under $ROOT to plain text. Note that this
# function should be called as "$self->_convert" to allow the
# DebugTexinfo::DebugCount::_convert method to override this one.
sub _convert($$)
{
my $self = shift;
my $root = shift;
my $formatter = $self->{'formatters'}->[-1];
if ($self->{'debug'}) {
my $is_top_formatter = 0;
$is_top_formatter = 1 if ($formatter->{'_top_formatter'});
my $empty_lines_count = '';
$empty_lines_count = $self->{'empty_lines_count'}
if defined($self->{'empty_lines_count'});
print STDERR "ROOT:$root (".join('|',@{$self->{'context'}})."), formatters ".scalar(@{$self->{'formatters'}}) . " ->";
print STDERR " cmd: $root->{'cmdname'}," if ($root->{'cmdname'});
print STDERR " type: $root->{'type'}" if ($root->{'type'});
my $text = $root->{'text'};
if (defined($text)) {
my $text_escaped_spaces
= Texinfo::Convert::Paragraph::_print_escaped_spaces($text);
print STDERR " text: $text_escaped_spaces";
}
print STDERR "\n";
print STDERR " empty_lines $empty_lines_count,top_fmter $is_top_formatter,format_ctxt $self->{'format_context'}->[-1]->{'cmdname'},para_cnt $self->{'format_context'}->[-1]->{'paragraph_count'},indent_lvl $self->{'format_context'}->[-1]->{'indent_level'},"
.(defined($self->{'text_element_context'}->[-1]->{'counter'}) ? "counter $self->{'text_element_context'}->[-1]->{'counter'}," : '')
."max $self->{'text_element_context'}->[-1]->{'max'}\n";
#print STDERR " Special def_command: $root->{'extra'}->{'def_command'}\n"
# if (defined($root->{'extra'}) and $root->{'extra'}->{'def_command'});
if ($formatter) {
my $monospace = $formatter->{'font_type_stack'}->[-1]->{'monospace'};
$monospace = 'UNDEF' if (!defined($monospace));
print STDERR " Container:($monospace,$formatter->{'upper_case'},$formatter->{'frenchspacing_stack'}->[-1]) ";
$formatter->{'container'}->dump();
}
}
if (($root->{'type'} and $self->{'ignored_types'}->{$root->{'type'}})
or ($root->{'cmdname'}
and ($self->{'ignored_commands'}->{$root->{'cmdname'}}
or ($inline_commands{$root->{'cmdname'}}
and $root->{'cmdname'} ne 'inlinefmtifelse'
and (($inline_format_commands{$root->{'cmdname'}}
and (!$root->{'extra'}->{'format'}
or !$self->{'expanded_formats_hash'}->{$root->{'extra'}->{'format'}}))
or (!$inline_format_commands{$root->{'cmdname'}}
and !defined($root->{'extra'}->{'expand_index'}))))))) {
print STDERR "IGNORED\n" if ($self->{'debug'});
return '';
}
my $result = '';
# in ignorable spaces, keep only form feeds.
if ($root->{'type'} and $self->{'ignorable_space_types'}->{$root->{'type'}}
and ($root->{'type'} ne 'empty_spaces_before_paragraph'
or $self->get_conf('paragraphindent') ne 'asis')) {
if ($root->{'text'} =~ /\f/) {
$result = _get_form_feeds($root->{'text'});
}
_add_text_count($self, $result);
print STDERR "IGNORABLE SPACE: ($result)\n" if ($self->{'debug'});
return $result;
}
# First handle empty lines. This has to be done before the handling
# of text below to be sure that an empty line is always processed
# especially
if ($root->{'type'} and ($root->{'type'} eq 'empty_line'
or $root->{'type'} eq 'after_description_line')) {
if ($self->{'debug'}) {
my $count = $self->{'empty_lines_count'};
$count = '' if (!defined($count));
print STDERR "EMPTY_LINE ($count)\n";
}
delete $self->{'text_element_context'}->[-1]->{'counter'};
$self->{'empty_lines_count'}++;
if ($self->{'empty_lines_count'} <= 1
or $self->{'preformatted_context_commands'}->{$self->{'context'}->[-1]}) {
$result = "";
if ($root->{'text'} =~ /\f/) {
$result .= _get_form_feeds($root->{'text'});
_add_text_count($self, $result);
}
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_text("\n"));
return $result;
} else {
return '';
}
}
# process text
if (defined($root->{'text'})) {
if (!$formatter->{'_top_formatter'}) {
if ($root->{'type'} and ($root->{'type'} eq 'raw'
or $root->{'type'} eq 'last_raw_newline')) {
$result = _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_next($root->{'text'}));
} else {
my $text = _process_text($self, $root, $formatter);
$result = _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_text($text));
}
return $result;
# the following is only possible if paragraphindent is set to asis
} elsif ($root->{'type'} and $root->{'type'} eq 'empty_spaces_before_paragraph') {
_add_text_count($self, $root->{'text'});
return $root->{'text'};
# ignore text outside of any format, but warn if ignored text not empty
} elsif ($root->{'text'} =~ /\S/) {
$self->_bug_message("ignored text not empty `$root->{'text'}'", $root);
return '';
}
}
if ($root->{'extra'}) {
#if ($root->{'extra'}->{'invalid_nesting'}) {
# print STDERR "INVALID_NESTING\n" if ($self->{'debug'});
# return '';
#} elsif ($root->{'extra'}->{'missing_argument'}
if ($root->{'extra'}->{'missing_argument'}
and (!$root->{'contents'} or !@{$root->{'contents'}})) {
print STDERR "MISSING_ARGUMENT\n" if ($self->{'debug'});
return '';
}
}
if ($root->{'extra'} and $root->{'extra'}->{'index_entry'}
and !$self->{'multiple_pass'} and !$self->{'in_copying_header'}) {
my $location = $self->_add_location($root);
# remove a 'lines' from $location if at the very end of a node
# since it will lead to the next node otherwise.
if ($root->{'cmdname'} and $root->{'cmdname'} =~ /index/) {
my $following_not_empty;
my @parents = @{$self->{'current_roots'}};
my @parent_contents = @{$self->{'current_contents'}};
while (@parents) {
my $parent = pop @parents;
my $parent_content = pop @parent_contents;
if ($parent->{'type'} and $parent->{'type'} eq 'paragraph') {
$following_not_empty = 1;
last;
}
foreach my $following_content (@$parent_content) {
unless (($following_content->{'type'}
and ($following_content->{'type'} eq 'empty_line'
or $ignorable_types{$following_content->{'type'}}))
or ($following_content->{'cmdname'}
and ($following_content->{'cmdname'} eq 'c'
or $following_content->{'cmdname'} eq 'comment'))) {
$following_not_empty = 1;
last;
}
}
last if $following_not_empty;
if ($parent->{'cmdname'} and $root_commands{$parent->{'cmdname'}}) {
last;
}
}
if (! $following_not_empty) {
print STDERR "INDEX ENTRY $root->{'cmdname'} followed by empty lines\n"
if ($self->{'debug'});
$location->{'lines'}--;
}
}
# this covers the special case for index entry not associated with a
# node but seen. this will be an index entry in @copying,
# in @insertcopying.
# This also covers the case of an index entry in a node added by a
# @footnote with footnotestyle separate.
if ($self->{'node'}) {
$location->{'node'} = $self->{'node'};
}
$self->{'index_entries_line_location'}->{$root} = $location;
print STDERR "INDEX ENTRY lines_count $location->{'lines'}, index_entry $location->{'index_entry'}\n"
if ($self->{'debug'});
}
my $cell;
my $preformatted;
if ($root->{'cmdname'}) {
my $unknown_command;
my $command = $root->{'cmdname'};
if (defined($no_brace_commands{$command})) {
if ($command eq ':') {
$formatter->{'container'}->remove_end_sentence();
return '';
} elsif ($command eq '*') {
$result = _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_pending_word());
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->end_line());
} elsif ($command eq '.' or $command eq '?' or $command eq '!') {
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_next($command));
$formatter->{'container'}->add_end_sentence(1);
} elsif ($command eq ' ' or $command eq "\n" or $command eq "\t") {
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_next($no_brace_commands{$command}));
} else {
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_text($no_brace_commands{$command}));
}
return $result;
} elsif ($root->{'cmdname'} eq 'today') {
my $today = $self->Texinfo::Common::expand_today();
unshift @{$self->{'current_contents'}->[-1]}, $today;
} elsif (exists($brace_no_arg_commands{$root->{'cmdname'}})) {
my $text;
if ($command eq 'dots' or $command eq 'enddots') {
# Don't use Unicode ellipsis character.
$text = '...';
} else {
$text = Texinfo::Convert::Text::brace_no_arg_command($root,
$self->{'convert_text_options'});
}
# @AA{} should suppress an end sentence, @aa{} shouldn't. This
# is the case whether we are in @sc or not.
if ($formatter->{'upper_case'}
and $letter_no_arg_commands{$root->{'cmdname'}}) {
$text = _protect_sentence_ends($text);
$text = uc($text);
if ($self->{'DEBUG'}) {
my $debug_text = $text;
$debug_text =~ s/\x08/!!/g;
print STDERR "accent markers:$debug_text\n";
}
}
if ($punctuation_no_arg_commands{$command}) {
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_next($text));
$formatter->{'container'}->add_end_sentence(1);
} elsif ($command eq 'tie') {
$formatter->{'w'}++;
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->set_space_protection(1,undef))
if ($formatter->{'w'} == 1);
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_text($text));
$formatter->{'w'}--;
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->set_space_protection(0,undef))
if ($formatter->{'w'} == 0);
} else {
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_text($text));
# This is to have @TeX{}, for example, not to prevent end sentences.
if (!$letter_no_arg_commands{$command}) {
$formatter->{'container'}->allow_end_sentence();
}
if ($command eq 'dots') {
$formatter->{'container'}->remove_end_sentence();
}
}
if ($formatter->{'var'}
or $formatter->{'font_type_stack'}->[-1]->{'monospace'}) {
$formatter->{'container'}->allow_end_sentence();
}
return $result;
# commands with braces
} elsif ($accent_commands{$root->{'cmdname'}}) {
my $encoding;
if ($self->{'enable_encoding'}) {
$encoding = $self->{'output_encoding_name'};
}
my $sc;
if ($formatter->{'upper_case'}) {
$sc = 1;
}
my $accented_text
= Texinfo::Convert::Text::text_accents($root, $encoding, $sc);
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_text($accented_text));
my $accented_text_original;
if ($formatter->{'upper_case'}) {
$accented_text_original
= Texinfo::Convert::Text::text_accents($root, $encoding);
}
if ($accented_text_original
and $accented_text_original !~ /[[:upper:]]/
or $formatter->{'var'}
or $formatter->{'font_type_stack'}->[-1]->{'monospace'}) {
$formatter->{'container'}->allow_end_sentence();
}
# in case the text added ends with punctuation.
# If the text is empty (likely because of an error) previous
# punctuation will be cancelled, we don't want that.
$formatter->{'container'}->remove_end_sentence()
if ($accented_text ne '');
return $result;
} elsif ($self->{'style_map'}->{$command}
or ($root->{'type'} and $root->{'type'} eq 'definfoenclose_command')) {
if ($code_style_commands{$command}) {
if (!$formatter->{'font_type_stack'}->[-1]->{'monospace'}) {
push @{$formatter->{'font_type_stack'}}, {'monospace' => 1};
} else {
$formatter->{'font_type_stack'}->[-1]->{'monospace'}++;
}
#$formatter->{'code'}++;
} elsif ($regular_font_style_commands{$command}) {
if ($formatter->{'font_type_stack'}->[-1]->{'monospace'}) {
push @{$formatter->{'font_type_stack'}}, {'monospace' => 0,
'normal' => 1};
} elsif ($formatter->{'font_type_stack'}->[-1]->{'normal'}) {
$formatter->{'font_type_stack'}->[-1]->{'normal'}++;
}
}
if ($no_punctation_munging_commands{$command}) {
push @{$formatter->{'frenchspacing_stack'}}, 'on';
$formatter->{'container'}->set_space_protection(undef,
undef,undef,1);
}
if ($upper_case_commands{$command}) {
$formatter->{'upper_case'}++;
$formatter->{'var'}++ if ($command eq 'var');
}
if ($command eq 'w') {
$formatter->{'w'}++;
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->set_space_protection(1,undef))
if ($formatter->{'w'} == 1);
}
my ($text_before, $text_after);
if ($root->{'type'} and $root->{'type'} eq 'definfoenclose_command') {
$text_before = $root->{'extra'}->{'begin'};
$text_after = $root->{'extra'}->{'end'};
} elsif ($non_quoted_commands_when_nested{$command}
and $formatter->{'font_type_stack'}->[-1]->{'code_command'}) {
$text_before = '';
$text_after = '';
} elsif ($formatter->{'suppress_styles'}
and !$index_style_commands{$command}) {
$text_before = '';
$text_after = '';
} else {
$text_before = $self->{'style_map'}->{$command}->[0];
$text_after = $self->{'style_map'}->{$command}->[1];
}
# do this after determining $text_before/$text_after such that it
# doesn't impact the current command, but only commands nested within
if ($non_quoted_commands_when_nested{$command}) {
$formatter->{'font_type_stack'}->[-1]->{'code_command'}++;
}
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_next($text_before, 1))
if ($text_before ne '');
if ($root->{'args'}) {
$result .= $self->_convert($root->{'args'}->[0]);
if ($root->{'cmdname'} eq 'strong'
and scalar (@{$root->{'args'}->[0]->{'contents'}})
and $root->{'args'}->[0]->{'contents'}->[0]->{'text'}
and $root->{'args'}->[0]->{'contents'}->[0]->{'text'} =~ /^Note\s/i
and $self->{'output_format'}
and $self->{'output_format'} eq 'info') {
$self->line_warn($self->__(
"\@strong{Note...} produces a spurious cross-reference in Info; reword to avoid that"),
$root->{'line_nr'});
}
}
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_next($text_after, 1))
if ($text_after ne '');
if ($command eq 'w') {
$formatter->{'w'}--;
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->set_space_protection(0,undef))
if ($formatter->{'w'} == 0);
}
if ($code_style_commands{$command}) {
#$formatter->{'code'}--;
$formatter->{'font_type_stack'}->[-1]->{'monospace'}--;
$formatter->{'container'}->allow_end_sentence();
pop @{$formatter->{'font_type_stack'}}
if !$formatter->{'font_type_stack'}->[-1]->{'monospace'};
} elsif ($regular_font_style_commands{$command}) {
if ($formatter->{'font_type_stack'}->[-1]->{'normal'}) {
$formatter->{'font_type_stack'}->[-1]->{'normal'}--;
pop @{$formatter->{'font_type_stack'}}
if !$formatter->{'font_type_stack'}->[-1]->{'normal'};
}
}
if ($non_quoted_commands_when_nested{$command}) {
#$formatter->{'code_command'}--;
$formatter->{'font_type_stack'}->[-1]->{'code_command'}--;
}
if ($no_punctation_munging_commands{$command}) {
pop @{$formatter->{'frenchspacing_stack'}};
my $frenchspacing = 0;
$frenchspacing = 1 if ($formatter->{'frenchspacing_stack'}->[-1] eq 'on');
$formatter->{'container'}->set_space_protection(undef,
undef, undef, $frenchspacing);
}
if ($upper_case_commands{$command}) {
$formatter->{'upper_case'}--;
if ($command eq 'var') {
$formatter->{'var'}--;
# Allow a following full stop to terminate a sentence.
$formatter->{'container'}->allow_end_sentence();
}
}
return $result;
} elsif ($root->{'cmdname'} eq 'image') {
$result = _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_pending_word(1));
my ($image, $lines_count) = $self->_image($root);
_add_lines_count($self, $lines_count);
_add_text_count($self, $image);
if ($image ne '' and $formatter->{'type'} ne 'paragraph') {
$self->{'empty_lines_count'} = 0;
}
$result .= $image;
return $result;
} elsif ($root->{'cmdname'} eq 'email') {
# nothing is output for email, instead the command is substituted.
my @email_contents;
if ($root->{'extra'} and $root->{'extra'}->{'brace_command_contents'}) {
my $name;
my $email;
if (scalar (@{$root->{'extra'}->{'brace_command_contents'}}) == 2
and defined($root->{'extra'}->{'brace_command_contents'}->[-1])) {
$name = $root->{'extra'}->{'brace_command_contents'}->[1];
}
if (defined($root->{'extra'}->{'brace_command_contents'}->[0])) {
$email = $root->{'extra'}->{'brace_command_contents'}->[0];
}
my $prepended;
if ($name and $email) {
$prepended = $self->gdt('{name} @url{{email}}',
{'name' => $name, 'email' => $email});
} elsif ($email) {
$prepended = $self->gdt('@url{{email}}',
{'email' => $email});
} elsif ($name) {
$prepended = {'contents' => $name};
} else {
return '';
}
unshift @{$self->{'current_contents'}->[-1]}, $prepended;
}
return '';
} elsif ($command eq 'uref' or $command eq 'url') {
if ($root->{'extra'} and $root->{'extra'}->{'brace_command_contents'}) {
if (scalar(@{$root->{'extra'}->{'brace_command_contents'}}) == 3
and defined($root->{'extra'}->{'brace_command_contents'}->[2])) {
unshift @{$self->{'current_contents'}->[-1]},
{'contents' => $root->{'extra'}->{'brace_command_contents'}->[2]};
} elsif (defined($root->{'extra'}->{'brace_command_contents'}->[0])) {
# no mangling of --- and similar in url.
my $url = {'type' => '_code',
'contents' => $root->{'extra'}->{'brace_command_contents'}->[0]};
if (scalar(@{$root->{'extra'}->{'brace_command_contents'}}) == 2
and defined($root->{'extra'}->{'brace_command_contents'}->[1])) {
my $prepended = $self->gdt('{text} ({url})',
{'text' => $root->{'extra'}->{'brace_command_contents'}->[1],
'url' => $url });
unshift @{$self->{'current_contents'}->[-1]}, $prepended;
} else {
my $prepended = $self->gdt('@t{<{url}>}',
{'url' => $url});
unshift @{$self->{'current_contents'}->[-1]}, $prepended
}
} elsif (scalar(@{$root->{'extra'}->{'brace_command_contents'}}) == 2
and defined($root->{'extra'}->{'brace_command_contents'}->[1])) {
unshift @{$self->{'current_contents'}->[-1]},
{'contents' => $root->{'extra'}->{'brace_command_contents'}->[1]};
}
}
return '';
} elsif ($command eq 'footnote') {
$self->{'footnote_index'}++ unless ($self->{'multiple_pass'});
my $formatted_footnote_number;
if ($self->get_conf('NUMBER_FOOTNOTES')) {
$formatted_footnote_number = $self->{'footnote_index'};
} else {
$formatted_footnote_number = $NO_NUMBER_FOOTNOTE_SYMBOL;
}
push @{$self->{'pending_footnotes'}}, {'root' => $root,
'number' => $self->{'footnote_index'}}
unless ($self->{'multiple_pass'});
if (!$self->{'in_copying_header'}) {
$self->_error_outside_of_any_node($root);
}
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_next
("($formatted_footnote_number)", 1));
if ($self->get_conf('footnotestyle') eq 'separate' and $self->{'node'}) {
$result .= $self->_convert({'contents' =>
[{'text' => ' ('},
{'cmdname' => 'pxref',
'extra' => {'brace_command_contents' =>
[[@{$self->{'node'}->{'extra'}->{'node_content'}},
{'text' => "-Footnote-$self->{'footnote_index'}"}]]}},
{'text' => ')'}],
});
}
return $result;
} elsif ($command eq 'anchor') {
$result = _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_pending_word());
$result .= $self->_anchor($root);
return $result;
} elsif ($ref_commands{$command}) {
if ($root->{'extra'} and $root->{'extra'}->{'brace_command_contents'}
and scalar(@{$root->{'extra'}->{'brace_command_contents'}})) {
my @args = @{$root->{'extra'}->{'brace_command_contents'}};
$args[0] = [{'text' => ''}] if (!defined($args[0]));
# normalize node name, to get a ref with the right formatting
# NOTE as a consequence, the line numbers appearing in case of errors
# correspond to the node lines numbers, and not the @ref.
my $node_content;
if ($root->{'extra'}
and $root->{'extra'}->{'label'}) {
$node_content = $root->{'extra'}->{'label'}->{'extra'}->{'node_content'};
} else {
$node_content = $args[0];
}
# if it a reference to a float with a label, $arg[1] is
# set to '$type $number' or '$number' if there is no type.
if (! defined($args[1])
and $root->{'extra'}
and $root->{'extra'}->{'label'}
and $root->{'extra'}->{'label'}->{'cmdname'}
and $root->{'extra'}->{'label'}->{'cmdname'} eq 'float') {
my $float = $root->{'extra'}->{'label'};
my $name = $self->_float_type_number($float);
$args[1] = $name->{'contents'};
}
if ($command eq 'inforef' and scalar(@args) == 3) {
$args[3] = $args[2];
$args[2] = undef;
}
# Treat cross-reference commands in a multitable cell as if they
# were surrounded by @w{ ... }, so the output will not be split across
# lines, leading text from other columns appearing to be part of the
# cross-reference.
my $in_multitable = 0;
if ($self->{'document_context'}->[-1]->{'in_multitable'}) {
$in_multitable = 1;
$formatter->{'w'}++;
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->set_space_protection(1,undef))
if ($formatter->{'w'} == 1);
}
# Disallow breaks in runs of Chinese text in node names, because a
# break would be normalized to a single space by the Info reader, and
# the node wouldn't be found.
$formatter->{'container'}
->set_space_protection(undef,undef,undef,undef,1); # double_width_no_break
if ($command eq 'xref') {
$result = $self->_convert({'contents' => [{'text' => '*Note '}]});
} else {
$result = $self->_convert({'contents' => [{'text' => '*note '}]});
}
my $name;
if (defined($args[1])) {
$name = $args[1];
} elsif (defined($args[2])) {
$name = $args[2];
}
my $file;
if (defined($args[3])) {
$file = [{'text' => '('},
{'type' => '_code',
'contents' => $args[3]},
{'text' => ')'},];
} elsif (defined($args[4])) {
# add a () such that the node is considered to be external,
# even though the manual name is not known.
$file = [{'text' => '()'}];
}
if ($name) {
my $name_text = $self->_convert({'contents' => $name});
# needed, as last word is added only when : is added below
my $name_text_checked = $name_text
.$self->{'formatters'}->[-1]->{'container'}->get_pending();
my $quoting_required = 0;
if ($name_text_checked =~ /:/m) {
if ($self->get_conf('INFO_SPECIAL_CHARS_WARNING')) {
$self->line_warn(sprintf($self->__(
"\@%s cross-reference name should not contain `:'"),
$command), $root->{'line_nr'});
}
if ($self->get_conf('INFO_SPECIAL_CHARS_QUOTE')) {
$quoting_required = 1;
}
}
my $pre_quote = $quoting_required ? "\x{7f}" : '';
my $post_quote = $pre_quote;
$name_text .= $self->_convert({'contents' => [
{'text' => "$post_quote: "}]});
$name_text =~ s/^(\s*)/$1$pre_quote/ if $pre_quote;
$result .= $name_text;
_count_added($self,$self->{'formatters'}[-1]{'container'},
$pre_quote)
if $pre_quote;
if ($file) {
$result .= $self->_convert({'contents' => $file});
}
# node name
$self->{'formatters'}->[-1]->{'suppress_styles'} = 1;
my $node_text = $self->_convert({'type' => '_code',
'contents' => $node_content});
delete $self->{'formatters'}->[-1]->{'suppress_styles'};
my $node_text_checked = $node_text
.$self->{'formatters'}->[-1]->{'container'}->get_pending();
$quoting_required = 0;
if ($node_text_checked =~ /([,\t\.])/m ) {
if ($self->get_conf('INFO_SPECIAL_CHARS_WARNING')) {
$self->line_warn(sprintf($self->__(
"\@%s node name should not contain `%s'"), $command, $1),
$root->{'line_nr'});
}
if ($self->get_conf('INFO_SPECIAL_CHARS_QUOTE')) {
$quoting_required = 1;
}
}
$pre_quote = $quoting_required ? "\x{7f}" : '';
$post_quote = $pre_quote;
if ($pre_quote) {
$node_text =~ s/^(\s*)/$1$pre_quote/;
_count_added($self,$self->{'formatters'}[-1]{'container'},
$pre_quote);
}
$result .= $node_text;
_count_added($self, $self->{'formatters'}[-1]{'container'},
$self->{'formatters'}->[-1]->{'container'}->add_next($post_quote))
if $post_quote;
} else { # Label same as node specification
if ($file) {
$result .= $self->_convert({'contents' => $file});
}
$self->{'formatters'}->[-1]->{'suppress_styles'} = 1;
my $node_text = $self->_convert({'type' => '_code',
'contents' => $node_content});
delete $self->{'formatters'}->[-1]->{'suppress_styles'};
my $node_text_checked = $node_text
.$self->{'formatters'}->[-1]->{'container'}->get_pending();
my $quoting_required = 0;
if ($node_text_checked =~ /:/m) {
if ($self->get_conf('INFO_SPECIAL_CHARS_WARNING')) {
$self->line_warn(sprintf($self->__(
"\@%s node name should not contain `:'"), $command),
$root->{'line_nr'});
}
if ($self->get_conf('INFO_SPECIAL_CHARS_QUOTE')) {
$quoting_required = 1;
}
}
my $pre_quote = $quoting_required ? "\x{7f}" : '';
my $post_quote = $pre_quote;
$node_text .= $self->_convert({'contents' => [
{'text' => "${post_quote}::"}]});
_count_added($self,$self->{'formatters'}[-1]{'container'},
$pre_quote)
if $pre_quote;
if ($pre_quote) {
# This is needed to get a pending word. We could use
# add_pending_word, but that would not include following
# punctuation in the word.
my $next = $self->{'current_contents'}->[-1]->[0];
if ($next) {
$node_text .= $self->_convert($next);
shift @{$self->{'current_contents'}->[-1]};
}
$node_text =~ s/^(\s*)/$1$pre_quote/;
}
$result .= $node_text;
}
# we could use $formatter, but in case it was changed in _convert
# we play it safe.
my $pending = $result
.$self->{'formatters'}->[-1]->{'container'}->get_pending();
# If command is @xref, the punctuation must always follow the
# command, for other commands it may be in the argument, hence the
# use of $pending.
if ($name and ($command eq 'xref'
or ($pending !~ /[\.,]$/ and $pending !~ /::$/))) {
my $next = $self->{'current_contents'}->[-1]->[0];
if (!($next and $next->{'text'} and $next->{'text'} =~ /^[\.,]/)) {
if ($command eq 'xref') {
if ($next and defined($next->{'text'}) and $next->{'text'} =~ /\S/) {
my $text = $next->{'text'};
$text =~ s/^\s*//;
my $char = substr($text, 0, 1);
$self->line_warn(sprintf($self->__(
"`.' or `,' must follow \@xref, not %s"),
$char), $root->{'line_nr'});
} else {
$self->line_warn($self->__("`.' or `,' must follow \@xref"),
$root->{'line_nr'});
}
}
my @added = ({'text' => '.'});
# the added dot do not end a sentence for pxref or ref.
push @added, {'cmdname' => ':'} if ($command ne 'xref');
unshift @{$self->{'current_contents'}->[-1]}, @added;
}
}
if ($in_multitable) {
$formatter->{'w'}--;
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->set_space_protection(0,undef))
if ($formatter->{'w'} == 0);
}
$formatter->{'container'}
->set_space_protection(undef,undef,undef,undef,0); # double_width_no_break
return $result;
}
return '';
} elsif ($explained_commands{$command}) {
if ($root->{'extra'} and $root->{'extra'}->{'brace_command_contents'}
and defined($root->{'extra'}->{'brace_command_contents'}->[0])) {
# in abbr spaces never end a sentence.
my $argument;
if ($command eq 'abbr') {
$argument = {'type' => 'frenchspacing',
'contents' => $root->{'extra'}->{'brace_command_contents'}->[0]};
} else {
$argument = {'contents' => $root->{'extra'}->{'brace_command_contents'}->[0]};
}
if (scalar (@{$root->{'extra'}->{'brace_command_contents'}}) == 2
and defined($root->{'extra'}->{'brace_command_contents'}->[-1])) {
my $prepended = $self->gdt('{abbr_or_acronym} ({explanation})',
{'abbr_or_acronym' => $argument,
'explanation' =>
$root->{'extra'}->{'brace_command_contents'}->[-1]});
#print STDERR "".Data::Dumper->Dump([$prepended])."\n";
unshift @{$self->{'current_contents'}->[-1]}, $prepended;
return '';
} else {
$result = $self->_convert($argument);
# We want to permit an end of sentence, but not force it as @. does.
$formatter->{'container'}->allow_end_sentence();
return $result;
}
}
return '';
} elsif ($inline_commands{$command}) {
my $arg_index = 1;
if ($command eq 'inlinefmtifelse'
and (!$root->{'extra'}->{'format'}
or !$self->{'expanded_formats_hash'}->{$root->{'extra'}->{'format'}})) {
$arg_index = 2;
}
if (scalar(@{$root->{'extra'}->{'brace_command_contents'}}) > $arg_index
and defined($root->{'extra'}->{'brace_command_contents'}->[$arg_index])) {
my $argument;
if ($command eq 'inlineraw') {
$argument->{'type'} = '_code';
}
$argument->{'contents'}
= $root->{'extra'}->{'brace_command_contents'}->[$arg_index];
unshift @{$self->{'current_contents'}->[-1]}, ($argument);
}
return '';
} elsif ($command eq 'math') {
push @{$self->{'context'}}, 'math';
if ($root->{'args'}) {
$result .= $self->_convert({'type' => 'frenchspacing',
'contents' => [{'type' => '_code',
'contents' => [$root->{'args'}->[0]]}]});
}
my $old_context = pop @{$self->{'context'}};
die if ($old_context ne 'math');
return $result;
} elsif ($command eq 'titlefont') {
push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0};
$result = $self->convert_line ({'type' => 'frenchspacing',
'contents' => [$root->{'args'}->[0]]});
pop @{$self->{'count_context'}};
$result = Texinfo::Convert::Text::heading({'level' => 0,
'cmdname' => 'titlefont'}, $result, $self,
$self->get_conf('NUMBER_SECTIONS'),
($self->{'format_context'}->[-1]->{'indent_level'}) * $indent_length);
$self->{'empty_lines_count'} = 0 unless ($result eq '');
_add_text_count($self, $result);
_add_lines_count($self, 2);
return $result;
} elsif ($command eq 'U') {
my $arg = $root->{'extra'}->{'brace_command_contents'}
->[0]->[0]->{'text'};
if (defined($arg) && $arg) {
# The general idea is to output UTF-8 if that has been
# explicitly given as the encoding, else simple ASCII.
#
# Syntactic checks on the value were already done in Parser.pm,
# but we have one more thing to test: since this is the one
# place where we might output actual UTF-8 binary bytes, we have
# to check that chr(hex($arg)) is valid. Perl gives a warning
# and will not output UTF-8 for Unicode non-characters such as
# U+10FFFF. In this case, silently fall back to plain text, on
# the theory that the user wants something.
#
# Having an option to output binary bytes nevertheless is
# possible, but seems unlikely to be practically useful, so skip
# it until it gets requested.
my $res;
if ($self->{'to_utf8'}) {
# The warning about non-characters is only given when the code
# point is attempted to be output, not just manipulated.
# http://stackoverflow.com/questions/5127725/how-could-i-catch-an-unicode-non-character-warning
#
# Therefore, we have to try to output it within an eval.
# Since opening /dev/null or a temporary file means
# more system-dependent checks, use a string as our
# filehandle; this was introduced ca.2000, which should be old
# enough. We hope.
eval {
use warnings FATAL => qw(all);
my ($fh, $string);
open($fh, ">", \$string) || die "open(U string eval) failed: $!";
binmode($fh, ":utf8") || die "binmode(U string eval) failed: $!";
print $fh chr(hex("$arg"));
};
if ($@) {
warn "\@U chr(hex($arg)) eval failed: $@\n" if ($self->{'DEBUG'});
$res = "U+$arg"; # chr won't work
} else {
$res = chr(hex($arg)); # ok to call chr
}
} else {
$res = "U+$arg"; # not outputting UTF-8
}
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_text($res));
} else {
$result = ''; # arg was not defined
}
return $result;
} elsif ($command eq 'value') {
my $expansion = $self->gdt('@{No value for `{value}\'@}',
{'value' => $root->{'type'}});
if ($formatter->{'_top_formatter'}) {
$expansion = {'type' => 'paragraph',
'contents' => [$expansion]};
}
$result .= $self->_convert($expansion);
# unshift @{$self->{'current_contents'}->[-1]}, $expansion;
#return '';
return $result;
} elsif ($root->{'args'} and $root->{'args'}->[0]
and $root->{'args'}->[0]->{'type'}
and $root->{'args'}->[0]->{'type'} eq 'brace_command_arg') {
print STDERR "Unknown command with braces `$root->{'cmdname'}'\n"
if ($self->get_conf('VERBOSE') or $self->{'debug'});
# block commands
} elsif (exists($block_commands{$root->{'cmdname'}})) {
# remark:
# cartouche group and raggedright -> nothing on format stack
if ($menu_commands{$root->{'cmdname'}} and !$self->get_conf('SHOW_MENU')) {
return '';
}
if ($self->{'preformatted_context_commands'}->{$root->{'cmdname'}}
or $root->{'cmdname'} eq 'float') {
if ($self->{'formatters'}->[-1]->{'type'} eq 'paragraph'
and $format_raw_commands{$root->{'cmdname'}}) {
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_pending_word(1));
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->end_line());
}
push @{$self->{'context'}}, $root->{'cmdname'};
} elsif ($flush_commands{$root->{'cmdname'}}) {
push @{$self->{'context'}}, $root->{'cmdname'};
} elsif ($raw_commands{$root->{'cmdname'}}) {
if (!$self->{'formatters'}->[-1]->{'_top_formatter'}) {
# reuse the current formatter if not in top level
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_pending_word(1));
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->end_line());
} else {
# if in top level, the raw block command is turned into a
# simple preformatted command (alike @verbatim), to have a
# formatter container being created.
push @{$self->{'context'}}, $root->{'cmdname'};
$self->{'format_context_commands'}->{$root->{'cmdname'}} = 1;
$self->{'preformatted_context_commands'}->{$root->{'cmdname'}} = 1;
}
}
if ($self->{'format_context_commands'}->{$root->{'cmdname'}}) {
push @{$self->{'format_context'}},
{ 'cmdname' => $root->{'cmdname'},
'paragraph_count' => 0,
'indent_level' =>
$self->{'format_context'}->[-1]->{'indent_level'},
};
$self->{'format_context'}->[-1]->{'indent_level'}++
if ($indented_commands{$root->{'cmdname'}});
# open a preformatted container, if the command opening the
# preformatted context is not a classical preformatted
# command (ie if it is menu or verbatim, and not example or
# similar)
if ($self->{'preformatted_context_commands'}->{$root->{'cmdname'}}
and ! $preformatted_commands{$root->{'cmdname'}}
and ! $format_raw_commands{$root->{'cmdname'}}) {
$preformatted = $self->new_formatter('unfilled');
push @{$self->{'formatters'}}, $preformatted;
}
}
if ($root->{'cmdname'} eq 'quotation'
or $root->{'cmdname'} eq 'smallquotation') {
if ($root->{'extra'} and $root->{'extra'}->{'block_command_line_contents'}) {
my $prepended = $self->gdt('@b{{quotation_arg}:} ',
{'quotation_arg' => $root->{'extra'}->{'block_command_line_contents'}->[0]});
#print STDERR Data::Dumper->Dump([$prepended]);
$prepended->{'type'} = 'frenchspacing';
$result .= $self->convert_line($prepended);
$self->{'text_element_context'}->[-1]->{'counter'} +=
Texinfo::Convert::Unicode::string_width($result);
$self->{'empty_lines_count'} = 0 unless ($result eq '');
}
} elsif ($menu_commands{$root->{'cmdname'}}) {
$result .= $self->_menu($root);
} elsif ($root->{'cmdname'} eq 'multitable') {
my $columnsize;
if ($root->{'extra'}->{'columnfractions'}) {
foreach my $fraction (@{$root->{'extra'}->{'columnfractions'}}) {
push @$columnsize, int($fraction * $self->{'text_element_context'}->[-1]->{'max'} +0.5);
}
} elsif ($root->{'extra'}->{'prototypes'}) {
foreach my $prototype (@{$root->{'extra'}->{'prototypes'}}) {
push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0};
my ($formatted_prototype) = $self->convert_line($prototype,
{'indent_length' => 0});
pop @{$self->{'count_context'}};
print STDERR " MULTITABLE_PROTO {$formatted_prototype}\n"
if ($self->{'debug'});
push @$columnsize,
2+Texinfo::Convert::Unicode::string_width($formatted_prototype);
}
}
print STDERR "MULTITABLE_SIZES @$columnsize\n" if ($columnsize
and $self->{'debug'});
$self->{'format_context'}->[-1]->{'columns_size'} = $columnsize;
$self->{'format_context'}->[-1]->{'row_empty_lines_count'}
= $self->{'empty_lines_count'};
$self->{'document_context'}->[-1]->{'in_multitable'}++;
} elsif ($root->{'cmdname'} eq 'float') {
$result .= _add_newline_if_needed($self);
if ($root->{'extra'} and $root->{'extra'}->{'normalized'}) {
$result .= $self->_anchor($root);
}
}
} elsif ($root->{'cmdname'} eq 'node') {
$self->{'node'} = $root;
$result .= $self->_node($root);
$self->{'format_context'}->[-1]->{'paragraph_count'} = 0;
} elsif ($sectioning_commands{$root->{'cmdname'}}) {
if ($self->get_conf('setcontentsaftertitlepage')
and $root_commands{$root->{'cmdname'}}
and !$self->{'setcontentsaftertitlepage_done'}) {
my ($contents, $lines_count)
= $self->_contents($self->{'structuring'}->{'sectioning_root'},
'contents');
if ($contents ne '') {
$contents .= "\n";
$self->{'empty_lines_count'} = 1;
_add_text_count($self, $contents);
_add_lines_count($self, $lines_count+1);
}
$self->{'setcontentsaftertitlepage_done'} = 1;
$result .= $contents;
}
if ($self->get_conf('setshortcontentsaftertitlepage')
and $root_commands{$root->{'cmdname'}}
and !$self->{'setshortcontentsaftertitlepage_done'}) {
my ($contents, $lines_count)
= $self->_contents($self->{'structuring'}->{'sectioning_root'},
'shortcontents');
if ($contents ne '') {
$contents .= "\n";
$self->{'empty_lines_count'} = 1;
_add_text_count($self, $contents);
_add_lines_count($self, $lines_count+1);
}
$self->{'setshortcontentsaftertitlepage_done'} = 1;
$result .= $contents;
}
# use settitle for empty @top
# ignore @part
my $contents;
if ($root->{'extra'}->{'misc_content'}
and @{$root->{'extra'}->{'misc_content'}}
and $root->{'cmdname'} ne 'part') {
$contents = $root->{'extra'}->{'misc_content'};
} elsif ($root->{'cmdname'} eq 'top'
and $self->{'extra'}->{'settitle'}
and $self->{'extra'}->{'settitle'}->{'extra'}
and $self->{'extra'}->{'settitle'}->{'extra'}->{'misc_content'}
and @{$self->{'extra'}->{'settitle'}->{'extra'}->{'misc_content'}}) {
$contents = $self->{'extra'}->{'settitle'}->{'extra'}->{'misc_content'};
}
if ($contents) {
push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0};
my $heading = $self->convert_line({'type' => 'frenchspacing',
'contents' => $contents});
pop @{$self->{'count_context'}};
# @* leads to an end of line, underlying appears on the line below
# over one line
my $heading_underlined =
Texinfo::Convert::Text::heading($root, $heading, $self,
$self->get_conf('NUMBER_SECTIONS'),
($self->{'format_context'}->[-1]->{'indent_level'})
* $indent_length);
$result .= _add_newline_if_needed($self);
$self->{'empty_lines_count'} = 0 unless ($heading_underlined eq '');
_add_text_count($self, $heading_underlined);
$result .= $heading_underlined;
if ($heading_underlined ne '') {
_add_lines_count($self, 2);
$result .= _add_newline_if_needed($self);
}
}
$self->{'format_context'}->[-1]->{'paragraph_count'} = 0;
} elsif (($root->{'cmdname'} eq 'item' or $root->{'cmdname'} eq 'itemx')
and $root->{'args'} and $root->{'args'}->[0]
and $root->{'args'}->[0]->{'type'}
and $root->{'args'}->[0]->{'type'} eq 'misc_line_arg') {
if ($root->{'extra'} and $root->{'extra'}->{'misc_content'}) {
my $converted_tree = $self->_table_item_content_tree($root,
$root->{'extra'}->{'misc_content'});
$converted_tree->{'type'} = 'frenchspacing';
$result = $self->convert_line($converted_tree,
{'indent_level'
=> $self->{'format_context'}->[-1]->{'indent_level'} -1});
if ($result ne '') {
$result = $self->ensure_end_of_line($result);
$self->{'empty_lines_count'} = 0;
}
}
} elsif ($root->{'cmdname'} eq 'item' and $root->{'parent'}->{'cmdname'}
and $item_container_commands{$root->{'parent'}->{'cmdname'}}) {
$self->{'format_context'}->[-1]->{'paragraph_count'} = 0;
my $line = $self->new_formatter('line',
{'indent_length' =>
($self->{'format_context'}->[-1]->{'indent_level'} -1)
* $indent_length
+ $item_indent_format_length{$root->{'parent'}->{'cmdname'}}});
push @{$self->{'formatters'}}, $line;
if ($root->{'parent'}->{'cmdname'} eq 'enumerate') {
$result = _count_added($self, $line->{'container'},
$line->{'container'}->add_next(
Texinfo::Common::enumerate_item_representation(
$root->{'parent'}->{'extra'}->{'enumerate_specification'},
$root->{'extra'}->{'item_number'}) . '. '));
} elsif ($root->{'parent'}->{'extra'}->{'block_command_line_contents'}) {
# this is the text prepended to items.
$result = $self->_convert(
{'contents' =>
[@{$root->{'parent'}->{'extra'}->{'block_command_line_contents'}->[0]},
{ 'text' => ' ' }]
});
}
$result .= _count_added($self, $line->{'container'},
$line->{'container'}->end());
print STDERR " $root->{'parent'}->{'cmdname'}($root->{'extra'}->{'item_number'}) -> |$result|\n"
if ($self->{'debug'});
pop @{$self->{'formatters'}};
$self->{'text_element_context'}->[-1]->{'counter'} +=
Texinfo::Convert::Unicode::string_width($result);
$self->{'empty_lines_count'} = 0 unless ($result eq '');
# open a multitable cell
} elsif ($root->{'cmdname'} eq 'headitem' or $root->{'cmdname'} eq 'item'
or $root->{'cmdname'} eq 'tab') {
my $cell_width = $self->{'format_context'}->[-1]->{'columns_size'}->[$root->{'extra'}->{'cell_number'}-1];
$self->{'format_context'}->[-1]->{'item_command'} = $root->{'cmdname'}
if ($root->{'cmdname'} ne 'tab');
print STDERR "CELL [$root->{'extra'}->{'cell_number'}]: \@$root->{'cmdname'}. Width: $cell_width\n"
if ($self->{'debug'});
die if (!defined($cell_width));
$self->{'empty_lines_count'}
= $self->{'format_context'}->[-1]->{'row_empty_lines_count'};
push @{$self->{'format_context'}},
{ 'cmdname' => $root->{'cmdname'},
'paragraph_count' => 0,
'indent_level' => 0 };
push @{$self->{'text_element_context'}}, {'max' => $cell_width - 2 };
push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0,
'locations' => []};
$cell = 1;
} elsif ($root->{'cmdname'} eq 'center') {
#my ($counts, $new_locations);
push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0,
'locations' => []};
$result = $self->convert_line (
{'type' => 'frenchspacing',
'contents' => $root->{'extra'}->{'misc_content'}},
{'indent_length' => 0});
if ($result ne '') {
$result = $self->ensure_end_of_line($result);
$result = $self->_align_environment ($result,
$self->{'text_element_context'}->[-1]->{'max'}, 'center');
$self->{'empty_lines_count'} = 0;
} else {
# it has to be done here, as it is done in _align_environment above
pop @{$self->{'count_context'}};
}
$self->{'format_context'}->[-1]->{'paragraph_count'}++;
return $result;
} elsif ($root->{'cmdname'} eq 'exdent') {
if ($self->{'preformatted_context_commands'}->{$self->{'context'}->[-1]}) {
$result = $self->convert_unfilled({'contents' => $root->{'extra'}->{'misc_content'}},
{'indent_level'
=> $self->{'format_context'}->[-1]->{'indent_level'} -1});
} else {
$result = $self->convert_line({'contents' => $root->{'extra'}->{'misc_content'}},
{'indent_level'
=> $self->{'format_context'}->[-1]->{'indent_level'} -1});
}
if ($result ne '') {
$result = $self->ensure_end_of_line($result);
$self->{'empty_lines_count'} = 0;
}
return $result;
} elsif ($root->{'cmdname'} eq 'verbatiminclude') {
my $expansion = $self->Texinfo::Common::expand_verbatiminclude($root);
unshift @{$self->{'current_contents'}->[-1]}, $expansion
if ($expansion);
return '';
} elsif ($root->{'cmdname'} eq 'insertcopying') {
if ($self->{'extra'} and $self->{'extra'}->{'copying'}) {
unshift @{$self->{'current_contents'}->[-1]},
{'contents' => $self->{'extra'}->{'copying'}->{'contents'}};
}
return '';
} elsif ($root->{'cmdname'} eq 'printindex') {
$result = $self->_printindex($root);
return $result;
} elsif ($root->{'cmdname'} eq 'listoffloats') {
my $lines_count = 0;
if ($root->{'extra'} and $root->{'extra'}->{'type'}
and defined($root->{'extra'}->{'type'}->{'normalized'})
and $self->{'floats'}
and $self->{'floats'}->{$root->{'extra'}->{'type'}->{'normalized'}}
and @{$self->{'floats'}->{$root->{'extra'}->{'type'}->{'normalized'}}}) {
push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0};
if (!$self->{'empty_lines_count'}) {
$result .= "\n";
$lines_count++;
}
$result .= "* Menu:\n\n";
$lines_count += 2;
foreach my $float (@{$self->{'floats'}->{$root->{'extra'}->{'type'}->{'normalized'}}}) {
next if (!defined($float->{'extra'}->{'block_command_line_contents'}->[1]));
my $float_label_text = $self->convert_line({'type' => '_code',
'contents'
=> $float->{'extra'}->{'block_command_line_contents'}->[1]});
my $float_entry = $self->_float_type_number($float);
my $float_entry_text = ':';
if (defined($float_entry)) {
$float_entry->{'type'} = 'frenchspacing';
$float_entry_text = $self->convert_line($float_entry);
}
# no translation here, this is required Info format.
my $float_line = "* $float_entry_text: $float_label_text.";
my $line_width
= Texinfo::Convert::Unicode::string_width($float_line);
if ($line_width > $listoffloat_entry_length) {
$float_line .= "\n" . ' ' x $listoffloat_entry_length;
$lines_count++;
} else {
$float_line .= ' ' x ($listoffloat_entry_length - $line_width);
}
$line_width = $listoffloat_entry_length;
my $caption;
if ($float->{'extra'}->{'shortcaption'}) {
$caption = $float->{'extra'}->{'shortcaption'};
} elsif ($float->{'extra'}->{'caption'}) {
$caption = $float->{'extra'}->{'caption'};
}
if ($caption) {
$self->{'multiple_pass'} = 1;
push @{$self->{'context'}}, 'listoffloats';
my $tree = {'contents' => $caption->{'args'}->[0]->{'contents'}};
# the following does nothing since there are paragraphs within
# the shortcaption.
#if ($caption->{'cmdname'} eq 'shortcaption') {
# $tree->{'type'} = 'frenchspacing';
#}
my $caption_text = $self->_convert($tree);
my $old_context = pop @{$self->{'context'}};
delete $self->{'multiple_pass'};
die if ($old_context ne 'listoffloats');
while ($caption_text =~ s/^\s*(\p{Unicode::EastAsianWidth::InFullwidth}\s*|\S+\s*)//) {
my $new_word = $1;
$new_word =~ s/\n//g;
if ((Texinfo::Convert::Unicode::string_width($new_word) +
$line_width) >
($self->{'text_element_context'}->[-1]->{'max'} - 3)) {
$float_line .= $listoffloat_append;
last;
} else {
$float_line .= $new_word;
$line_width +=
Texinfo::Convert::Unicode::string_width($new_word);
}
}
}
$result .= $float_line. "\n";
$lines_count++;
}
$result .= "\n";
$lines_count++;
$self->{'empty_lines_count'} = 1;
pop @{$self->{'count_context'}};
}
$self->{'format_context'}->[-1]->{'paragraph_count'}++;
_add_text_count($self, $result);
_add_lines_count($self, $lines_count);
return $result;
} elsif ($root->{'cmdname'} eq 'sp') {
if ($root->{'extra'}->{'misc_args'}->[0]) {
# this useless copy avoids perl changing the type to integer!
my $sp_nr = $root->{'extra'}->{'misc_args'}->[0];
for (my $i = 0; $i < $sp_nr; $i++) {
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->end_line());
}
$self->{'empty_lines_count'} += $sp_nr;
delete $self->{'text_element_context'}->[-1]->{'counter'};
}
return $result;
} elsif ($root->{'cmdname'} eq 'contents') {
if (!defined($self->get_conf('setcontentsaftertitlepage'))
and $self->{'structuring'}
and $self->{'structuring'}->{'sectioning_root'}) {
my $lines_count;
($result, $lines_count)
= $self->_contents($self->{'structuring'}->{'sectioning_root'},
'contents');
_add_lines_count($self, $lines_count);
_add_text_count($self, $result);
}
return $result;
} elsif ($root->{'cmdname'} eq 'shortcontents'
or $root->{'cmdname'} eq 'summarycontents') {
if (!defined($self->get_conf('setshortcontentsaftertitlepage'))
and $self->{'structuring'}
and $self->{'structuring'}->{'sectioning_root'}) {
my $lines_count;
($result, $lines_count)
= $self->_contents($self->{'structuring'}->{'sectioning_root'},
'shortcontents');
_add_lines_count($self, $lines_count);
_add_text_count($self, $result);
}
return $result;
# all the @-commands that have an information for the formatting, like
# @paragraphindent, @frenchspacing...
} elsif ($informative_commands{$root->{'cmdname'}}) {
$self->_informative_command($root);
return '';
} else {
$unknown_command = 1;
}
if ($unknown_command and !($root->{'extra'}
and $root->{'extra'}->{'index_entry'})
# commands like def*x are not processed above, since only the def_line
# associated is processed. If they have no name and no category they
# are not considered as index entries either so they have a specific
# condition
and !($def_commands{$root->{'cmdname'}}
and $root->{'cmdname'} =~ /x$/)) {
warn "Unhandled $root->{'cmdname'}\n";
$result .= "!!!!!!!!! Unhandled $root->{'cmdname'} !!!!!!!!!\n";
}
}
# open 'type' constructs.
my $paragraph;
if ($root->{'type'}) {
if ($root->{'type'} eq 'paragraph') {
$self->{'empty_lines_count'} = 0;
my $conf;
# indent. Not first paragraph.
if ($self->{'debug'}) {
print STDERR "OPEN PARA ($self->{'format_context'}->[-1]->{'cmdname'}) "
. "cnt ".
(defined($self->{'text_element_context'}->[-1]->{'counter'}) ?
$self->{'text_element_context'}->[-1]->{'counter'} : 'UNDEF'). ' '
. "para cnt $self->{'format_context'}->[-1]->{'paragraph_count'} "
. "fparaindent ".$self->get_conf('firstparagraphindent')." "
. "paraindent ".$self->get_conf('paragraphindent')."\n";
}
if ($self->{'format_context'}->[-1]->{'cmdname'} eq '_top_format'
and $self->get_conf('paragraphindent') ne 'asis'
and $self->get_conf('paragraphindent')
and (($root->{'extra'} and $root->{'extra'}->{'indent'})
or (!($root->{'extra'} and $root->{'extra'}->{'noindent'})
and ($self->{'format_context'}->[-1]->{'paragraph_count'}
or $self->get_conf('firstparagraphindent') eq 'insert')
and !$self->{'text_element_context'}->[-1]->{'counter'}))) {
$conf->{'first_indent_length'} = $self->get_conf('paragraphindent');
$conf->{'first_indent_length'} = 0
if ($conf->{'first_indent_length'} eq 'none');
}
$paragraph = $self->new_formatter('paragraph', $conf);
push @{$self->{'formatters'}}, $paragraph;
$self->{'format_context'}->[-1]->{'paragraph_count'}++;
if ($self->{'context'}->[-1] eq 'flushright') {
push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0,
'locations' => []};
}
} elsif ($root->{'type'} eq 'preformatted'
or $root->{'type'} eq 'rawpreformatted') {
# if in a description reuse the main menu unfilled, to keep things
# simpler and avoid having to do a separate count.
if ($root->{'type'} eq 'rawpreformatted'
or !$root->{'parent'}->{'type'}
or $root->{'parent'}->{'type'} ne 'menu_entry_description') {
$preformatted = $self->new_formatter('unfilled');
push @{$self->{'formatters'}}, $preformatted;
if ($self->{'context'}->[-1] eq 'flushright') {
push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0,
'locations' => []};
}
}
} elsif ($root->{'type'} eq 'def_line') {
if ($root->{'extra'} and $root->{'extra'}->{'def_args'}
and @{$root->{'extra'}->{'def_args'}}) {
my $arguments = Texinfo::Common::definition_arguments_content($root);
my $tree;
my $command;
if ($Texinfo::Common::def_aliases{$root->{'extra'}->{'def_command'}}) {
$command = $Texinfo::Common::def_aliases{$root->{'extra'}->{'def_command'}};
} else {
$command = $root->{'extra'}->{'def_command'};
}
my $name;
if ($root->{'extra'}->{'def_parsed_hash'}->{'name'}) {
$name = $root->{'extra'}->{'def_parsed_hash'}->{'name'};
} else {
$name = '';
}
if ($command eq 'deffn'
or $command eq 'defvr'
or $command eq 'deftp'
or (($command eq 'deftypefn'
or $command eq 'deftypevr')
and !$root->{'extra'}->{'def_parsed_hash'}->{'type'})) {
if ($arguments) {
$tree = $self->gdt("\@tie{ }-- {category}: {name} {arguments}", {
'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
'name' => $name,
'arguments' => $arguments});
} else {
$tree = $self->gdt("\@tie{ }-- {category}: {name}", {
'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
'name' => $name});
}
} elsif ($command eq 'deftypefn'
or $command eq 'deftypevr') {
if ($arguments) {
my $strings = {
'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
'name' => $name,
'type' => $root->{'extra'}->{'def_parsed_hash'}->{'type'},
'arguments' => $arguments};
if ($self->get_conf('deftypefnnewline') eq 'on') {
$tree = $self->gdt("\@tie{ }-- {category}:\@*{type}\@*{name} {arguments}",
$strings);
} else {
$tree = $self->gdt("\@tie{ }-- {category}: {type} {name} {arguments}",
$strings);
}
} else {
my $strings = {
'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
'type' => $root->{'extra'}->{'def_parsed_hash'}->{'type'},
'name' => $name};
if ($self->get_conf('deftypefnnewline') eq 'on') {
$tree = $self->gdt("\@tie{ }-- {category}:\@*{type}\@*{name}",
$strings);
} else {
$tree = $self->gdt("\@tie{ }-- {category}: {type} {name}",
$strings);
}
}
} elsif ($command eq 'defcv'
or ($command eq 'deftypecv'
and !$root->{'extra'}->{'def_parsed_hash'}->{'type'})) {
if ($arguments) {
$tree = $self->gdt("\@tie{ }-- {category} of {class}: {name} {arguments}", {
'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
'name' => $name,
'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
'arguments' => $arguments});
} else {
$tree = $self->gdt("\@tie{ }-- {category} of {class}: {name}", {
'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
'name' => $name});
}
} elsif ($command eq 'defop'
or ($command eq 'deftypeop'
and !$root->{'extra'}->{'def_parsed_hash'}->{'type'})) {
if ($arguments) {
$tree = $self->gdt("\@tie{ }-- {category} on {class}: {name} {arguments}", {
'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
'name' => $name,
'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
'arguments' => $arguments});
} else {
$tree = $self->gdt("\@tie{ }-- {category} on {class}: {name}", {
'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
'name' => $name});
}
} elsif ($command eq 'deftypeop') {
if ($arguments) {
my $strings = {
'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
'name' => $name,
'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
'type' => $root->{'extra'}->{'def_parsed_hash'}->{'type'},
'arguments' => $arguments};
if ($self->get_conf('deftypefnnewline') eq 'on') {
$tree
= $self->gdt("\@tie{ }-- {category} on {class}:\@*{type}\@*{name} {arguments}",
$strings);
} else {
$tree
= $self->gdt("\@tie{ }-- {category} on {class}: {type} {name} {arguments}",
$strings);
}
} else {
my $strings = {
'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
'type' => $root->{'extra'}->{'def_parsed_hash'}->{'type'},
'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
'name' => $name};
if ($self->get_conf('deftypefnnewline') eq 'on') {
$tree
= $self->gdt("\@tie{ }-- {category} on {class}:\@*{type}\@*{name}",
$strings);
} else {
$tree
= $self->gdt("\@tie{ }-- {category} on {class}: {type} {name}",
$strings);
}
}
} elsif ($command eq 'deftypecv') {
if ($arguments) {
my $strings = {
'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
'name' => $name,
'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
'type' => $root->{'extra'}->{'def_parsed_hash'}->{'type'},
'arguments' => $arguments};
if ($self->get_conf('deftypefnnewline') eq 'on') {
$tree
= $self->gdt("\@tie{ }-- {category} of {class}:\@*{type}\@*{name} {arguments}",
$strings);
} else {
$tree
= $self->gdt("\@tie{ }-- {category} of {class}: {type} {name} {arguments}",
$strings);
}
} else {
my $strings = {
'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
'type' => $root->{'extra'}->{'def_parsed_hash'}->{'type'},
'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
'name' => $name};
if ($self->get_conf('deftypefnnewline') eq 'on') {
$tree
= $self->gdt("\@tie{ }-- {category} of {class}:\@*{type}\@*{name}",
$strings);
} else {
$tree
= $self->gdt("\@tie{ }-- {category} of {class}: {type} {name}",
$strings);
}
}
}
my $def_paragraph = $self->new_formatter('paragraph',
{ 'indent_length' => ($self->{'format_context'}->[-1]->{'indent_level'} -1) *$indent_length,
'indent_length_next' => (1+$self->{'format_context'}->[-1]->{'indent_level'})*$indent_length});
push @{$self->{'formatters'}}, $def_paragraph;
$result .= $self->_convert({'type' => '_code', 'contents' => [$tree]});
$result .= _count_added($self, $def_paragraph->{'container'},
$def_paragraph->{'container'}->end());
pop @{$self->{'formatters'}};
delete $self->{'text_element_context'}->[-1]->{'counter'};
$self->{'empty_lines_count'} = 0;
print STDERR " --> $result" if ($self->{'debug'});
}
} elsif ($root->{'type'} eq 'menu_entry') {
#my $menu_entry_internal_node;
#if ($root->{'extra'} and $root->{'extra'}->{'menu_entry_node'}
# and defined($root->{'extra'}->{'menu_entry_node'}->{'normalized'})
# and !$root->{'extra'}->{'menu_entry_node'}->{'manual_content'}
# and $self->{'labels'}
# and $self->{'labels'}->{$root->{'extra'}->{'menu_entry_node'}->{'normalized'}}) {
# $menu_entry_internal_node
# = $self->{'labels'}->{$root->{'extra'}->{'menu_entry_node'}->{'normalized'}};
#}
my $entry_name_seen = 0;
foreach my $arg (@{$root->{'args'}}) {
my ($pre_quote, $post_quote);
if ($arg->{'type'} eq 'menu_entry_node') {
$self->{'formatters'}->[-1]->{'suppress_styles'} = 1;
my $node_text = $self->_convert({'type' => '_code',
'contents' => $arg->{'contents'}});
delete $self->{'formatters'}->[-1]->{'suppress_styles'};
$pre_quote = $post_quote = '';
if ($entry_name_seen) {
if ($node_text =~ /([,\t]|\.\s)/) {
if ($self->get_conf('INFO_SPECIAL_CHARS_WARNING')) {
$self->line_warn(sprintf($self->__(
"menu entry node name should not contain `%s'"), $1),
$root->{'line_nr'});
}
}
if ($self->get_conf('INFO_SPECIAL_CHARS_QUOTE')) {
$pre_quote = $post_quote = "\x{7f}";
}
} else {
if ($node_text =~ /:/) {
if ($self->get_conf('INFO_SPECIAL_CHARS_WARNING')) {
$self->line_warn($self->__(
"menu entry node name should not contain `:'"),
$root->{'line_nr'});
}
if ($self->get_conf('INFO_SPECIAL_CHARS_QUOTE')) {
$pre_quote = $post_quote = "\x{7f}";
}
}
}
$result .= $pre_quote . $node_text . $post_quote;
} elsif ($arg->{'type'} eq 'menu_entry_name') {
my $entry_name = $self->_convert($arg);
$entry_name_seen = 1;
$pre_quote = $post_quote = '';
if ($entry_name =~ /:/) {
if ($self->get_conf('INFO_SPECIAL_CHARS_WARNING')) {
$self->line_warn($self->__(
"menu entry name should not contain `:'"),
$root->{'line_nr'});
}
if ($self->get_conf('INFO_SPECIAL_CHARS_QUOTE')) {
$pre_quote = $post_quote = "\x{7f}";
}
}
$result .= $pre_quote . $entry_name . $post_quote;
} else {
$result .= $self->_convert($arg);
}
}
$result = $self->ensure_end_of_line($result)
unless ($root->{'parent'}->{'type'}
and $root->{'parent'}->{'type'} eq 'preformatted');
} elsif ($root->{'type'} eq 'frenchspacing') {
push @{$formatter->{'frenchspacing_stack'}}, 'on';
$formatter->{'container'}->set_space_protection(undef,
undef,undef,1);
} elsif ($root->{'type'} eq '_code') {
#$formatter->{'code'}++;
if (!$formatter->{'font_type_stack'}->[-1]->{'monospace'}) {
push @{$formatter->{'font_type_stack'}}, {'monospace' => 1};
} else {
$formatter->{'font_type_stack'}->[-1]->{'monospace'}++;
}
push @{$formatter->{'frenchspacing_stack'}}, 'on';
$formatter->{'container'}->set_space_protection(undef,
undef,undef,1);
} elsif ($root->{'type'} eq 'bracketed') {
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_text('{'));
}
}
# The processing of contents is done here.
if ($root->{'contents'}) {
my @contents = @{$root->{'contents'}};
push @{$self->{'current_contents'}}, \@contents;
push @{$self->{'current_roots'}}, $root;
while (@contents) {
my $content = shift @contents;
my $text = $self->_convert($content);
$self->{'empty_lines_count'} = 0
if ($preformatted and $text =~ /\S/);
$result .= $text;
}
pop @{$self->{'current_contents'}};
pop @{$self->{'current_roots'}};
}
# now closing. First, close types.
if ($root->{'type'}) {
if ($root->{'type'} eq 'frenchspacing') {
pop @{$formatter->{'frenchspacing_stack'}};
my $frenchspacing = 0;
$frenchspacing = 1 if ($formatter->{'frenchspacing_stack'}->[-1] eq 'on');
$formatter->{'container'}->set_space_protection(undef,
undef, undef, $frenchspacing);
} elsif ($root->{'type'} eq '_code') {
#$formatter->{'code'}--;
$formatter->{'font_type_stack'}->[-1]->{'monospace'}--;
pop @{$formatter->{'font_type_stack'}}
if !$formatter->{'font_type_stack'}->[-1]->{'monospace'};
pop @{$formatter->{'frenchspacing_stack'}};
my $frenchspacing = 0;
$frenchspacing = 1 if ($formatter->{'frenchspacing_stack'}->[-1] eq 'on');
$formatter->{'container'}->set_space_protection(undef,
undef, undef, $frenchspacing);
} elsif ($root->{'type'} eq 'bracketed') {
$result .= _count_added($self, $formatter->{'container'},
$formatter->{'container'}->add_text('}'));
} elsif ($root->{'type'} eq 'row') {
my @cell_beginnings;
my @cell_lines;
my $cell_beginning = 0;
my $cell_idx = 0;
my $max_lines = 0;
my $indent_len = $indent_length
* $self->{'format_context'}->[-1]->{'indent_level'};
foreach my $cell (@{$self->{'format_context'}->[-1]->{'row'}}) {
$cell_beginnings[$cell_idx] = $cell_beginning;
my $cell_width = $self->{'format_context'}->[-1]->{'columns_size'}->[$cell_idx];
$cell_beginning += $cell_width +1;
$cell_lines[$cell_idx] = [ split /^/, $cell ];
$max_lines = scalar(@{$cell_lines[$cell_idx]})
if (scalar(@{$cell_lines[$cell_idx]}) > $max_lines);
$cell_idx++;
}
$cell_idx = 0;
my $cell_updated_locations;
my @row_locations;
foreach my $cell_locations (@{$self->{'format_context'}->[-1]->{'row_counts'}}) {
foreach my $location (@{$cell_locations->{'locations'}}) {
next unless (defined($location->{'bytes'}) and defined($location->{'lines'}));
push @{$cell_updated_locations->[$cell_idx]->{$location->{'lines'}}},
$location;
print STDERR "MULTITABLE anchor $location->{'root'}->{'extra'}->{'normalized'}: c $cell_idx, l $location->{'lines'} ($location->{'bytes'})\n"
if ($self->{'debug'});
$max_lines = $location->{'lines'}+1
if ($location->{'lines'}+1 > $max_lines);
}
push @row_locations, @{$cell_locations->{'locations'}};
$cell_idx++;
}
print STDERR "ROW, max_lines $max_lines, indent_len $indent_len\n"
if ($self->{'debug'});
# this is used to keep track of the last cell with content.
my $max_cell = scalar(@{$self->{'format_context'}->[-1]->{'row'}});
my $bytes_count = 0;
my $line;
for (my $line_idx = 0; $line_idx < $max_lines; $line_idx++) {
my $line_width = $indent_len;
$line = '';
# determine the last cell in the line, to fill spaces in
# cells preceding that cell on the line
my $last_cell = 0;
for (my $cell_idx = 0; $cell_idx < $max_cell; $cell_idx++) {
$last_cell = $cell_idx+1 if (defined($cell_lines[$cell_idx]->[$line_idx])
or defined($cell_updated_locations->[$cell_idx]->{$line_idx}));
}
print STDERR " L(last_cell $last_cell): $line_idx\n"
if ($self->{'debug'});
for (my $cell_idx = 0; $cell_idx < $last_cell; $cell_idx++) {
my $cell_text = $cell_lines[$cell_idx]->[$line_idx];
if (defined($cell_text)) {
chomp($cell_text);
if ($line eq '' and $cell_text ne '') {
$line = ' ' x $indent_len;
$bytes_count += $self->count_bytes($line);
}
print STDERR " C($cell_idx) `$cell_text'\n" if ($self->{'debug'});
$line .= $cell_text;
$bytes_count += $self->count_bytes($cell_text);
$line_width += Texinfo::Convert::Unicode::string_width($cell_text);
}
if (defined($cell_updated_locations->[$cell_idx]->{$line_idx})) {
foreach my $location (@{$cell_updated_locations->[$cell_idx]->{$line_idx}}) {
print STDERR "MULTITABLE UPDATE ANCHOR (l $line_idx, c $cell_idx): $location->{'root'}->{'extra'}->{'normalized'}: $location->{'bytes'} -> $bytes_count\n"
if ($self->{'debug'});
$location->{'bytes'} = $bytes_count;
}
}
if ($cell_idx+1 < $last_cell) {
if ($line_width < $indent_len + $cell_beginnings[$cell_idx+1]) {
if ($line eq '') {
$line = ' ' x $indent_len;
$bytes_count += $self->count_bytes($line);
}
my $spaces = ' ' x ($indent_len + $cell_beginnings[$cell_idx+1] - $line_width);
$line_width += Texinfo::Convert::Unicode::string_width($spaces);
$line .= $spaces;
$bytes_count += $self->count_bytes($spaces);
}
}
}
$line .= "\n";
$bytes_count += $self->count_bytes("\n");
$result .= $line;
}
if ($self->{'format_context'}->[-1]->{'item_command'} eq 'headitem') {
# at this point cell_beginning is at the beginning of
# the cell following the end of the table -> full width
my $line = ' ' x $indent_len . '-' x $cell_beginning . "\n";
$bytes_count += $self->count_bytes($line);
$result .= $line;
$self->{'empty_lines_count'} = 0;
$max_lines++;
# there may be empty lines, in that case $line is undef, $max_lines == 0
} elsif ($max_lines) {
if ($line eq "\n") {
$self->{'empty_lines_count'} = 1;
} else {
$self->{'empty_lines_count'} = 0;
}
}
$self->_update_locations_counts(\@row_locations);
push @{$self->{'count_context'}->[-1]->{'locations'}}, @row_locations;
$self->{'count_context'}->[-1]->{'bytes'} += $bytes_count;
$self->{'count_context'}->[-1]->{'lines'} += $max_lines;
$self->{'format_context'}->[-1]->{'row'} = [];
$self->{'format_context'}->[-1]->{'row_counts'} = [];
$self->{'format_context'}->[-1]->{'row_empty_lines_count'}
= $self->{'empty_lines_count'};
} elsif ($root->{'type'} eq 'text_root') {
$self->{'text_before_first_node'} = $result;
}
}
# close paragraphs and preformatted
if ($paragraph) {
$result .= _count_added($self, $paragraph->{'container'},
$paragraph->{'container'}->end());
if ($self->{'context'}->[-1] eq 'flushright') {
$result = $self->_align_environment($result,
$self->{'text_element_context'}->[-1]->{'max'}, 'right');
}
pop @{$self->{'formatters'}};
delete $self->{'text_element_context'}->[-1]->{'counter'};
} elsif ($preformatted) {
$result .= _count_added($self, $preformatted->{'container'},
$preformatted->{'container'}->end());
if ($result ne '') {
$result = $self->ensure_end_of_line($result);
}
if ($self->{'context'}->[-1] eq 'flushright') {
$result = $self->_align_environment ($result,
$self->{'text_element_context'}->[-1]->{'max'}, 'right');
}
pop @{$self->{'formatters'}};
# We assume that, upon closing the preformatted we are at the
# beginning of a line.
delete $self->{'text_element_context'}->[-1]->{'counter'};
}
# close commands
if ($root->{'cmdname'}) {
if ($root->{'cmdname'} eq 'float') {
if ($self->{'debug'}) {
my $type_texi = '';
$type_texi = Texinfo::Convert::Texinfo::convert({'contents' => $root->{'extra'}->{'type'}->{'content'}})
if ($root->{'extra'} and $root->{'extra'}->{'type'}->{'normalized'} ne '');
my $number = '';
$number = $root->{'number'} if (defined($root->{'number'}));
print STDERR "FLOAT: ($number) ($type_texi)\n";
}
if ($root->{'extra'}
and ($root->{'extra'}->{'type'}->{'normalized'} ne ''
or defined($root->{'number'})
or $root->{'extra'}->{'caption'} or $root->{'extra'}->{'shortcaption'})) {
$result .= _add_newline_if_needed($self);
my ($caption, $prepended) = Texinfo::Common::float_name_caption($self,
$root);
if ($prepended) {
#print STDERR "PREPENDED ".Data::Dumper->Dump([$prepended]);
$prepended->{'type'} = 'frenchspacing';
my $float_number = $self->convert_line ($prepended);
$result .= $float_number;
$self->{'text_element_context'}->[-1]->{'counter'} +=
Texinfo::Convert::Unicode::string_width($float_number);
$self->{'empty_lines_count'} = 0;
}
if ($caption) {
$self->{'format_context'}->[-1]->{'paragraph_count'} = 0;
my $tree = $caption->{'args'}->[0];
$result .= $self->_convert($tree);
}
}
} elsif (($root->{'cmdname'} eq 'quotation'
or $root->{'cmdname'} eq 'smallquotation')
and $root->{'extra'} and $root->{'extra'}->{'authors'}) {
foreach my $author (@{$root->{'extra'}->{'authors'}}) {
$result .= $self->_convert(
$self->gdt("\@center --- \@emph{{author}}\n",
{'author' => $author->{'extra'}->{'misc_content'}}));
}
} elsif (($root->{'cmdname'} eq 'multitable')) {
$self->{'document_context'}->[-1]->{'in_multitable'}--;
}
# close the contexts and register the cells
if ($self->{'preformatted_context_commands'}->{$root->{'cmdname'}}
or $root->{'cmdname'} eq 'float') {
my $old_context = pop @{$self->{'context'}};
die "Not a preformatted context: $old_context"
if (!$self->{'preformatted_context_commands'}->{$old_context}
and $old_context ne 'float');
if ($old_context ne 'float' and !$menu_commands{$old_context}) {
$self->{'empty_lines_count'} = 0;
}
delete ($self->{'preformatted_context_commands'}->{$root->{'cmdname'}})
unless ($default_preformatted_context_commands{$root->{'cmdname'}});
} elsif ($flush_commands{$root->{'cmdname'}}) {
my $old_context = pop @{$self->{'context'}};
die if (! $flush_commands{$old_context});
}
if ($self->{'format_context_commands'}->{$root->{'cmdname'}}) {
pop @{$self->{'format_context'}};
delete ($self->{'format_context_commands'}->{$root->{'cmdname'}})
unless ($default_format_context_commands{$root->{'cmdname'}});
} elsif ($cell) {
pop @{$self->{'format_context'}};
pop @{$self->{'text_element_context'}};
push @{$self->{'format_context'}->[-1]->{'row'}}, $result;
_update_count_context($self);
my $cell_counts = pop @{$self->{'count_context'}};
push @{$self->{'format_context'}->[-1]->{'row_counts'}}, $cell_counts;
$result = '';
}
if ($advance_paragraph_count_commands{$root->{'cmdname'}}) {
$self->{'format_context'}->[-1]->{'paragraph_count'}++;
}
}
return $result;
}
sub indent_one_menu_descriptions($$)
{
my $self = shift;
my $menu = shift;
foreach my $content (@{$menu->{'contents'}}) {
if ($content->{'type'} and $content->{'type'} eq 'menu_entry') {
my $result = '';
my $node_seen = 0;
foreach my $arg (@{$content->{'args'}}) {
if ($arg->{'type'} eq 'menu_entry_node') {
$result .= $self->_convert({'type' => '_code',
'contents' => $arg->{'contents'}});
$node_seen = 1;
} else {
# the separator appearing after the node is modified
if ($arg->{'type'} eq 'menu_entry_separator' and $node_seen) {
$arg->{'text'} =~ s/\s*$//;
}
$result .= $self->_convert($arg);
if ($arg->{'type'} eq 'menu_entry_separator' and $node_seen) {
my $length = Texinfo::Convert::Unicode::string_width($result);
my $description_indent
= $self->get_conf('TEXINFO_COLUMN_FOR_DESCRIPTION');
if ($length >= $description_indent) {
$arg->{'text'} .= ' ';
} else {
$arg->{'text'} .= ' ' x ($description_indent - $length);
}
last;
} elsif ($arg->{'type'} eq 'menu_entry_description') {
# This should never happen, but this is a safeguard for
# incorrect trees.
last;
}
}
}
#print STDERR "$result";
}
}
}
# FIXME document
sub indent_menu_descriptions($;$)
{
my $self = shift;
my $parser = shift;
if (!defined($self)) {
# setup a converter for menu
if (!defined($parser)) {
return undef;
}
$self = Texinfo::Convert::Plaintext->converter({'parser' => $parser});
} elsif (!defined($parser)) {
if (!defined($self->{'parser'})) {
return undef;
}
$parser = $self->{'parser'};
}
# setup the converter as if it was in a menu
my $cmdname = 'menu';
push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0,
'locations' => []};
push @{$self->{'context'}}, $cmdname;
push @{$self->{'format_context'}},
{ 'cmdname' => $cmdname,
'paragraph_count' => 0,
'indent_level' =>
$self->{'format_context'}->[-1]->{'indent_level'},
};
my $preformatted = $self->new_formatter('unfilled');
push @{$self->{'formatters'}}, $preformatted;
if ($parser->{'info'} and $parser->{'info'}->{'unassociated_menus'}) {
foreach my $menu (@{$parser->{'info'}->{'unassociated_menus'}}) {
$self->indent_one_menu_descriptions($menu);
}
}
if ($parser->{'nodes'} and @{$parser->{'nodes'}}) {
foreach my $node (@{$parser->{'nodes'}}) {
if ($node->{'menus'}) {
foreach my $menu (@{$node->{'menus'}}) {
$self->indent_menu_descriptions($menu);
}
}
}
}
pop @{$self->{'formatters'}};
pop @{$self->{'context'}};
pop @{$self->{'format_context'}};
pop @{$self->{'count_context'}};
}
1;
__END__
# $Id: Plaintext.pm 6991 2016-02-06 12:16:13Z gavin $
# Automatically generated from maintain/template.pod
=head1 NAME
Texinfo::Convert::Plaintext - Convert Texinfo tree to Plaintext
=head1 SYNOPSIS
my $converter
= Texinfo::Convert::Plaintext->converter({'parser' => $parser});
$converter->output($tree);
$converter->convert($tree);
$converter->convert_tree($tree);
=head1 DESCRIPTION
Texinfo::Convert::Plaintext converts a Texinfo tree to Plaintext.
=head1 METHODS
=over
=item $converter = Texinfo::Convert::Plaintext->converter($options)
Initialize converter from Texinfo to Plaintext.
The I<$options> hash reference holds options for the converter. In
this option hash reference a parser object may be associated with the
I<parser> key. The other options should be configuration options
described in the Texinfo manual. Those options, when appropriate,
override the document content.
See L<Texinfo::Convert::Converter> for more informations.
=item $converter->output($tree)
Convert a Texinfo tree I<$tree> and output the result in files as
described in the Texinfo manual.
=item $result = $converter->convert($tree)
Convert a Texinfo tree I<$tree> or tree portion and return
the resulting output.
=item $result = $converter->convert_tree($tree)
Convert a Texinfo tree portion I<$tree> and return the resulting
output. This function does not try to output a full document but only
portions. For a full document use C<convert>.
=back
=head1 AUTHOR
Patrice Dumas, E<lt>pertusus@free.frE<gt>
=head1 COPYRIGHT AND LICENSE
Copyright 2015 Free Software Foundation, Inc.
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