1# Plaintext.pm: output tree as text with filling.
2#
3# Copyright 2010-2020 Free Software Foundation, Inc.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 3 of the License,
8# or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17#
18# Original author: Patrice Dumas <pertusus@free.fr>
19
20package Texinfo::Convert::Plaintext;
21
22use 5.00405;
23
24# See comment at start of HTML.pm
25use if $] >= 5.012, feature => qw(unicode_strings);
26
27use strict;
28
29use Texinfo::Convert::Converter;
30use Texinfo::Common;
31use Texinfo::Convert::Texinfo;
32use Texinfo::Convert::Paragraph;
33
34use Texinfo::Convert::Text;
35
36require Exporter;
37use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
38@ISA = qw(Texinfo::Convert::Converter);
39
40# Some extra initialization for the first time this module is loaded.
41# This could be done in a UNITCHECK block, introduced in Perl 5.10.
42our $module_loaded = 0;
43sub import {
44  if (!$module_loaded) {
45    Texinfo::XSLoader::override(
46      "Texinfo::Convert::Plaintext::_process_text_internal",
47      "Texinfo::MiscXS::process_text");
48    $module_loaded = 1;
49  }
50  # The usual import method
51  goto &Exporter::import;
52}
53
54%EXPORT_TAGS = ( 'all' => [ qw(
55  convert
56  output
57) ] );
58
59@EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
60
61@EXPORT = qw(
62);
63
64$VERSION = '6.8';
65
66# misc commands that are of use for formatting.
67my %formatting_misc_commands = %Texinfo::Convert::Text::formatting_misc_commands;
68
69my $NO_NUMBER_FOOTNOTE_SYMBOL = '*';
70
71my @informative_global_commands = ('paragraphindent', 'firstparagraphindent',
72'frenchspacing', 'documentencoding', 'footnotestyle', 'documentlanguage',
73'contents', 'shortcontents', 'summarycontents', 'deftypefnnewline');
74
75my %informative_commands;
76foreach my $informative_command (@informative_global_commands) {
77  $informative_commands{$informative_command} = 1;
78}
79
80my %no_brace_commands = %Texinfo::Common::no_brace_commands;
81my %brace_no_arg_commands;
82foreach my $command (keys (%Texinfo::Common::brace_commands)) {
83  $brace_no_arg_commands{$command} = 1
84    if ($Texinfo::Common::brace_commands{$command} eq '0');
85}
86my %accent_commands = %Texinfo::Common::accent_commands;
87my %misc_commands = %Texinfo::Common::misc_commands;
88my %sectioning_commands = %Texinfo::Common::sectioning_commands;
89my %def_commands = %Texinfo::Common::def_commands;
90my %ref_commands = %Texinfo::Common::ref_commands;
91my %block_commands = %Texinfo::Common::block_commands;
92my %menu_commands = %Texinfo::Common::menu_commands;
93my %root_commands = %Texinfo::Common::root_commands;
94my %preformatted_commands = %Texinfo::Common::preformatted_commands;
95my %math_commands = %Texinfo::Common::math_commands;
96my %explained_commands = %Texinfo::Common::explained_commands;
97my %inline_format_commands = %Texinfo::Common::inline_format_commands;
98my %inline_commands = %Texinfo::Common::inline_commands;
99my %item_container_commands = %Texinfo::Common::item_container_commands;
100my %raw_commands = %Texinfo::Common::raw_commands;
101my %format_raw_commands = %Texinfo::Common::format_raw_commands;
102my %code_style_commands       = %Texinfo::Common::code_style_commands;
103my %regular_font_style_commands = %Texinfo::Common::regular_font_style_commands;
104my %preformatted_code_commands = %Texinfo::Common::preformatted_code_commands;
105my %default_index_commands = %Texinfo::Common::default_index_commands;
106my %letter_no_arg_commands = %Texinfo::Common::letter_no_arg_commands;
107
108foreach my $kept_command(keys (%informative_commands),
109  keys (%default_index_commands),
110  'verbatiminclude', 'insertcopying',
111  'listoffloats', 'printindex', ) {
112  $formatting_misc_commands{$kept_command} = 1;
113}
114
115foreach my $def_command (keys(%def_commands)) {
116  $formatting_misc_commands{$def_command} = 1 if ($misc_commands{$def_command});
117}
118
119# There are 6 stacks that define the context.
120# context:   relevant for alignement of text.  Set in math, footnote,
121#            listoffloats, flush_commands, preformatted_context_commands
122#            (preformatted + menu + verbatim), and raw commands if
123#            on top level.
124# format_context: used for the count of paragraphs and for the indentation.
125#            Set in footnote, for all commands relevant for indenting, like
126#            @*table, @itemize, @enumerate, preformatted commands,
127#            @*quotation, @def*, and also menu commands, @flushleft,
128#            @flushright, @float, in multitable cell and raw commands if at
129#            toplevel.
130# text_element_context: for the max columns and the counter in the line
131#            position (although the counter in the line is taken over by
132#            the formatter once a formatter is opened).
133#            Set in footnote and in multitable cells.
134# formatters: a formatter environment has stacks for formatting context.
135#            Also holds a container, an objects that does the counting
136#            of columns, actual indentation.  In general, it is better not
137#            to have formatters in parallel, but it may happen.
138# count_context: holds the bytes count, the lines count and the location
139#            of the commands that have their byte count or lines count
140#            recorded.  It is set for out of document formatting to avoid
141#            counting some converted text, but it is also set when it has
142#            to be modified afterwards, for aligned commands or multitable
143#            cells for example.
144# document_context: Used to keep track if we are in a multitable.
145
146# formatters have their own stack
147# in container
148# 'upper_case'
149# 'var'
150# 'font_type_stack'
151#
152# paragraph number incremented with paragraphs, center, listoffloats
153# and block commands except: html and such, group, raggedright, menu*, float
154
155my %default_preformatted_context_commands = (%preformatted_commands,
156                                             %format_raw_commands);
157foreach my $preformatted_command ('verbatim', keys(%menu_commands)) {
158  $default_preformatted_context_commands{$preformatted_command} = 1;
159}
160
161my %block_math_commands;
162foreach my $block_math_command (keys(%math_commands)) {
163  if (exists($block_commands{$block_math_command})) {
164    $block_math_commands{$block_math_command} = 1;
165  }
166}
167
168my %ignored_misc_commands;
169foreach my $misc_command (keys(%misc_commands)) {
170  $ignored_misc_commands{$misc_command} = 1
171    unless ($formatting_misc_commands{$misc_command});
172}
173
174my %ignored_commands = %ignored_misc_commands;
175foreach my $ignored_brace_commands ('caption', 'shortcaption',
176  'hyphenation', 'sortas') {
177  $ignored_commands{$ignored_brace_commands} = 1;
178}
179
180my %item_indent_format_length = ('enumerate' => 2,
181    'itemize' => 3,
182    'table' => 0,
183    'vtable' => 0,
184    'ftable' => 0,
185 );
186
187my $indent_length = 5;
188
189my %indented_commands;
190foreach my $indented_command (keys(%item_indent_format_length),
191           keys(%preformatted_commands), 'quotation', 'smallquotation',
192           'indentedblock', 'smallindentedblock',
193           keys(%def_commands)) {
194  $indented_commands{$indented_command} = 1
195    if exists($block_commands{$indented_command});
196}
197
198my %default_format_context_commands = %indented_commands;
199
200foreach my $non_indented('format', 'smallformat') {
201  delete $indented_commands{$non_indented};
202}
203
204# FIXME should keys(%math_brace_commands) be added here?
205# How can this be tested?
206foreach my $format_context_command (keys(%menu_commands), 'verbatim',
207 'flushleft', 'flushright', 'multitable', 'float') {
208  $default_format_context_commands{$format_context_command} = 1;
209}
210
211my %flush_commands = (
212  'flushleft'  => 1,
213  'flushright' => 1
214);
215
216# commands that leads to advancing the paragraph number.  This is mostly
217# used to determine the first line, in fact.
218my %advance_paragraph_count_commands;
219foreach my $command (keys(%block_commands)) {
220  next if ($menu_commands{$command}
221            or $block_commands{$command} eq 'raw');
222  $advance_paragraph_count_commands{$command} = 1;
223}
224
225# group and raggedright do more than not advancing para, they should also
226# be transparent with respect to paragraph number counting.
227foreach my $not_advancing_para ('group', 'raggedright',
228  'titlepage', 'copying', 'documentdescription', 'float') {
229  delete $advance_paragraph_count_commands{$not_advancing_para};
230}
231
232foreach my $advancing_para('center', 'verbatim', 'listoffloats') {
233  $advance_paragraph_count_commands{$advancing_para} = 1;
234}
235
236foreach my $ignored_block_commands ('ignore', 'macro', 'rmacro', 'copying',
237  'documentdescription', 'titlepage', 'direntry') {
238  $ignored_commands{$ignored_block_commands} = 1;
239}
240
241my %punctuation_no_arg_commands;
242foreach my $punctuation_command('enddots', 'exclamdown', 'questiondown') {
243  $punctuation_no_arg_commands{$punctuation_command} = 1;
244}
245
246my %upper_case_commands = (
247 'sc' => 1,
248 'var' => 1
249);
250
251my %ignorable_space_types;
252foreach my $type ('empty_line_after_command',
253            'empty_spaces_after_command', 'spaces_at_end',
254            'empty_spaces_before_argument', 'empty_spaces_before_paragraph',
255            'empty_spaces_after_close_brace') {
256  $ignorable_space_types{$type} = 1;
257}
258
259my %ignored_types;
260foreach my $type ('preamble',
261            'preamble_before_setfilename') {
262  $ignored_types{$type} = 1;
263}
264
265my %ignorable_types = %ignorable_space_types;
266foreach my $ignored_type(keys(%ignored_types)) {
267  $ignorable_types{$ignored_type} = 1;
268}
269
270# All those commands run with the text.
271my %style_map = (
272  'strong' => '*',
273  'dfn'    => '"',
274  'emph'   => '_',
275);
276
277foreach my $command (keys(%style_map)) {
278  $style_map{$command} = [$style_map{$command}, $style_map{$command}];
279}
280
281# math is special
282my @asis_commands = ('asis', 'w', 'b', 'i', 'sc', 't', 'r',
283  'slanted', 'sansserif', 'var', 'verb', 'clicksequence',
284  'headitemfont', 'dmn');
285
286foreach my $asis_command (@asis_commands) {
287  $style_map{$asis_command} = ['', ''];
288}
289
290my @quoted_commands = ('cite', 'code', 'command', 'env', 'file', 'kbd',
291  'option', 'samp', 'indicateurl');
292
293# %non_quoted_commands_when_nested have no quote when in code command contexts
294my %non_quoted_commands_when_nested;
295
296# Quotes are reset in converter_initialize and unicode quotes are used
297# if @documentencoding utf-8 is used.
298foreach my $quoted_command (@quoted_commands) {
299  $style_map{$quoted_command} = ["'", "'"];
300  if ($code_style_commands{$quoted_command}) {
301    $non_quoted_commands_when_nested{$quoted_command} = 1;
302  }
303}
304# always quoted even when nested
305delete $non_quoted_commands_when_nested{'samp'};
306delete $non_quoted_commands_when_nested{'indicateurl'};
307
308$style_map{'key'} = ['<', '>'];
309$style_map{'sub'} = ['_{', '}'];
310$style_map{'sup'} = ['^{', '}'];
311
312# Commands producing styles that are output in node names and index entries.
313my %index_style_commands;
314for my $index_style_command ('strong', 'emph', 'sub', 'sup', 'key') {
315  $index_style_commands{$index_style_command} = 1;
316}
317
318
319# in those commands, there is no addition of double space after a dot.
320# math is special
321my %no_punctation_munging_commands;
322foreach my $command ('var', 'cite', 'dmn', keys(%code_style_commands)) {
323  $no_punctation_munging_commands{$command} = 1;
324}
325
326my %defaults = (
327  'ENABLE_ENCODING'      => 1,
328  'FORMAT_MENU'          => 'nomenu',
329  #'EXTENSION'            => 'info',
330  'EXTENSION'            => 'txt',
331  #'USE_SETFILENAME_EXTENSION' => 1,
332  'INFO_SPECIAL_CHARS_WARNING' => 1,
333
334  #'OUTFILE'              => undef,
335  'OUTFILE'              => '-',
336  'SUBDIR'               => undef,
337  'documentlanguage'     => undef,
338
339  'output_format'        => '',
340  'USE_NODES'            => 1,
341);
342
343sub push_top_formatter($$)
344{
345  my ($self, $top_context) = @_;
346
347  push @{$self->{'context'}}, $top_context;
348  push @{$self->{'format_context'}}, {
349                                     'cmdname' => '_top_format',
350                                     'indent_level' => 0,
351                                     'paragraph_count' => 0,
352                                   };
353  push @{$self->{'text_element_context'}}, {
354                                     'max' => $self->{'fillcolumn'}
355                                   };
356  push @{$self->{'document_context'}}, {
357                                     'in_multitable' => 0
358                                   };
359
360  # This is not really meant to be used, as contents should open
361  # their own formatters, however it happens that there is some text
362  # outside any content that needs to be formatted, as @sp for example.
363  push @{$self->{'formatters'}}, $self->new_formatter('line');
364  $self->{'formatters'}->[-1]->{'_top_formatter'} = 1;
365}
366
367
368my %contents_commands = (
369 'contents' => 1,
370 'shortcontents' => 1,
371 'summarycontents' => 1,
372);
373
374sub converter_defaults($$)
375{
376  return %defaults;
377}
378
379sub converter_global_commands($)
380{
381  return @informative_global_commands;
382}
383
384sub converter_initialize($)
385{
386  my $self = shift;
387
388  $self->{'context'} = [];
389  $self->{'format_context'} = [];
390  $self->{'empty_lines_count'} = undef;
391  push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0,
392                                     'locations' => []};
393
394  %{$self->{'ignored_types'}} = %ignored_types;
395  %{$self->{'ignorable_space_types'}} = %ignorable_space_types;
396  %{$self->{'ignored_commands'}} = %ignored_commands;
397  # this is dynamic because raw formats may either be full commands if
398  # isolated, or simple text if in a paragraph
399  %{$self->{'format_context_commands'}} = %default_format_context_commands;
400  %{$self->{'preformatted_context_commands'}}
401     = %default_preformatted_context_commands;
402  $self->{'footnote_index'} = 0;
403  $self->{'pending_footnotes'} = [];
404
405  foreach my $format (keys(%format_raw_commands)) {
406    $self->{'ignored_commands'}->{$format} = 1
407       unless ($self->{'expanded_formats_hash'}->{$format});
408  }
409
410  %{$self->{'style_map'}} = %style_map;
411  $self->{'convert_text_options'}
412      = {Texinfo::Common::_convert_text_options($self)};
413
414  if ($self->get_conf('ENABLE_ENCODING') and $self->get_conf('OUTPUT_ENCODING_NAME')
415      and $self->get_conf('OUTPUT_ENCODING_NAME') eq 'utf-8') {
416    # cache this to avoid redoing calls to get_conf
417    $self->{'to_utf8'} = 1;
418    if (!$self->{'extra'}->{'documentencoding'}) {
419      # Do not use curly quotes or some other unnecessary non-ASCII characters
420      # if '@documentencoding UTF-8' is not given.
421      $self->{'convert_text_options'}->{'no_extra_unicode'} = 1;
422    } else {
423      foreach my $quoted_command (@quoted_commands) {
424        # Directed single quotes
425        $self->{'style_map'}->{$quoted_command} = ["\x{2018}", "\x{2019}"];
426      }
427      # Directed double quotes
428      $self->{'style_map'}->{'dfn'} = ["\x{201C}", "\x{201D}"];
429    }
430  }
431  if (defined($self->get_conf('OPEN_QUOTE_SYMBOL'))) {
432    foreach my $quoted_command (@quoted_commands) {
433      $self->{'style_map'}->{$quoted_command}->[0]
434       = $self->get_conf('OPEN_QUOTE_SYMBOL');
435    }
436  }
437  if (defined($self->get_conf('CLOSE_QUOTE_SYMBOL'))) {
438    foreach my $quoted_command (@quoted_commands) {
439      $self->{'style_map'}->{$quoted_command}->[1]
440       = $self->get_conf('CLOSE_QUOTE_SYMBOL');
441    }
442  }
443  if ($self->get_conf('FILLCOLUMN')) {
444    $self->{'fillcolumn'} = $self->get_conf('FILLCOLUMN');
445    # else it's already set via the defaults
446  }
447  # This needs to be here to take into account $self->{'fillcolumn'}.
448  $self->push_top_formatter('_Root_context');
449  # some caching to avoid calling get_conf
450  if ($self->get_conf('OUTPUT_PERL_ENCODING')) {
451    $self->{'output_perl_encoding'} = $self->get_conf('OUTPUT_PERL_ENCODING');
452  } else {
453    $self->{'output_perl_encoding'} = '';
454  }
455  $self->{'enable_encoding'} = $self->get_conf('ENABLE_ENCODING');
456  $self->{'output_encoding_name'} = $self->get_conf('OUTPUT_ENCODING_NAME');
457  $self->{'debug'} = $self->get_conf('DEBUG');
458
459  return $self;
460}
461
462sub _count_context_bug_message($$$)
463{
464  my ($self, $precision, $element) = @_;
465
466  if (scalar(@{$self->{'count_context'}}) != 1) {
467    my $element_text;
468    if ($element) {
469      $element_text
470         = Texinfo::Structuring::_print_element_command_texi($element);
471    } else {
472      $element_text = '';
473    }
474    $self->_bug_message("Too much count_context ${precision}(".
475      scalar(@{$self->{'count_context'}}). "): ". $element_text, $element);
476    die;
477  }
478}
479
480sub _convert_element($$)
481{
482  my ($self, $element) = @_;
483
484  my $result = '';
485  $result .= $self->_convert($element);
486  $self->_count_context_bug_message('', $element);
487  $result .= $self->_footnotes($element);
488  $self->_count_context_bug_message('footnotes ', $element);
489
490  return $result;
491}
492
493sub convert($$)
494{
495  my ($self, $root) = @_;
496
497  my $result = '';
498
499  my $elements = Texinfo::Structuring::split_by_node($root);
500  $self->{'empty_lines_count'} = 1;
501  if (!defined($elements)) {
502    $result = $self->_convert($root);
503    $self->_count_context_bug_message('no element ');
504    my $footnotes = $self->_footnotes();
505    $self->_count_context_bug_message('no element footnotes ');
506    $result .= $footnotes;
507  } else {
508    foreach my $node (@$elements) {
509      my $node_text = _convert_element($self, $node);
510      $result .= $node_text;
511    }
512  }
513
514  return $result;
515}
516
517sub convert_tree($$)
518{
519  my ($self, $root) = @_;
520
521  $self->{'empty_lines_count'} = 1;
522  my $result;
523  if ($root->{'type'} and $root->{'type'} eq 'element') {
524    $result = _convert_element($self, $root);
525  } else {
526    $result = $self->_convert($root);
527  }
528  return $result;
529}
530
531my $end_sentence = quotemeta('.?!');
532my $after_punctuation = quotemeta('"\')]');
533
534sub _protect_sentence_ends ($) {
535  my $text = shift;
536  # Avoid suppressing end of sentence, by inserting a control character
537  # in front of the full stop.  The choice of BS for this is arbitrary.
538  $text =~ s/(?<=[^[:upper:]])
539             (?=[$end_sentence][$after_punctuation]*(?:\s|$))
540             /\x08/gx;
541
542  # Also insert a control character at end of string, to protect a full stop
543  # that may follow later.
544
545  #$text =~ s/(?<=[^[:upper:]][$after_punctuation]*)$/\x08/;
546  # Perl doesn't support "variable length lookbehind"
547
548  $text = reverse $text;
549  $text =~ s/^(?=[$after_punctuation]*
550                 (?:[^[:upper:]\s]|[\x{202f}\x{00a0}]))
551            /\x08/x;
552  $text = reverse $text;
553
554  return $text;
555}
556
557sub _process_text_internal {
558  my ($text) = @_;
559
560  $text =~ s/---/\x{1F}/g;
561  $text =~ s/--/-/g;
562  $text =~ s/\x{1F}/--/g;
563  $text =~ s/``/"/g;
564  $text =~ s/\'\'/"/g;
565  $text =~ s/`/'/g;
566
567  return $text;
568}
569
570# Convert ``, '', `, ', ---, -- in $COMMAND->{'text'} to their output,
571# possibly coverting to upper case as well.
572sub _process_text($$$)
573{
574  my ($self, $command, $context) = @_;
575
576  my $text = $command->{'text'};
577
578  if ($context->{'upper_case'}
579      or $self->{'formatters'}[-1]->{'var'}) {
580    $text = _protect_sentence_ends($text);
581    $text = uc($text);
582  }
583
584  if ($self->{'to_utf8'}
585      and $self->{'extra'}->{'documentencoding'}) {
586    return Texinfo::Convert::Unicode::unicode_text($text,
587            $context->{'font_type_stack'}->[-1]->{'monospace'});
588  } elsif (!$context->{'font_type_stack'}->[-1]->{'monospace'}) {
589    return _process_text_internal($text);
590  }
591  return $text;
592}
593
594sub new_formatter($$;$)
595{
596  my ($self, $type, $conf) = @_;
597
598  my $first_indent_length = $conf->{'first_indent_length'};
599  delete $conf->{'first_indent_length'};
600
601  my $container;
602  my $container_conf = {
603         'max'               => $self->{'text_element_context'}->[-1]->{'max'},
604         'indent_level'      => $self->{'format_context'}->[-1]->{'indent_level'},
605  };
606  $container_conf->{'frenchspacing'} = 1
607    if ($self->{'conf'}->{'frenchspacing'} eq 'on');
608  $container_conf->{'counter'}
609    = $self->{'text_element_context'}->[-1]->{'counter'}
610      if (defined($self->{'text_element_context'}->[-1]->{'counter'}));
611  $container_conf->{'DEBUG'} = 1 if ($self->{'debug'});
612  if ($conf) {
613    foreach my $key (keys(%$conf)) {
614      $container_conf->{$key} = $conf->{$key};
615    }
616  }
617  my $indent = $container_conf->{'indent_length'};
618  $indent = $indent_length*$container_conf->{'indent_level'}
619    if (!defined($indent));
620
621  if ($first_indent_length) {
622    $container_conf->{'indent_length'} = $first_indent_length;
623    $container_conf->{'indent_length_next'} = $indent;
624  } else {
625    $container_conf->{'indent_length'} = $indent;
626  }
627
628  if ($type eq 'line') {
629    $container_conf->{'max'} = 10000001;
630    $container_conf->{'keep_end_lines'} = 1;
631    $container_conf->{'no_final_newline'} = 1;
632    $container_conf->{'add_final_space'} = 1;
633
634    $container = Texinfo::Convert::Paragraph->new($container_conf);
635  } elsif ($type eq 'paragraph') {
636    $container = Texinfo::Convert::Paragraph->new($container_conf);
637  } elsif ($type eq 'unfilled') {
638    $container_conf->{'max'} = 10000000;
639    $container_conf->{'ignore_columns'} = 1;
640    $container_conf->{'keep_end_lines'} = 1;
641    $container_conf->{'frenchspacing'} = 1;
642    $container_conf->{'unfilled'} = 1;
643    $container_conf->{'no_final_newline'} = 1;
644
645    $container = Texinfo::Convert::Paragraph->new($container_conf);
646  } else {
647    die "Unknown container type $type\n";
648  }
649
650  if ($flush_commands{$self->{'context'}->[-1]}) {
651    set_space_protection($container, undef, 1, 1);
652  }
653
654  my $formatter = {'container' => $container, 'upper_case' => 0,
655                   'font_type_stack' => [{}],
656                   'w' => 0, 'type' => $type,
657              'frenchspacing_stack' => [$self->{'conf'}->{'frenchspacing'}],
658              'suppress_styles' => $conf->{'suppress_styles'}};
659
660  if ($type eq 'unfilled') {
661    foreach my $context (reverse(@{$self->{'context'}})) {
662      if ($menu_commands{$context}) {
663        last;
664      } elsif ($preformatted_code_commands{$context}
665               or $format_raw_commands{$context}
666               or $math_commands{$context}) {
667        $formatter->{'font_type_stack'}->[-1]->{'monospace'} = 1;
668        $formatter->{'font_type_stack'}->[-1]->{'code_command'} = 1
669          if ($preformatted_code_commands{$context}
670              or $math_commands{$context});
671        last;
672      }
673    }
674  }
675  return $formatter;
676}
677
678sub convert_line($$;$)
679{
680  my ($self, $converted, $conf) = @_;
681  my $formatter = $self->new_formatter('line', $conf);
682  push @{$self->{'formatters'}}, $formatter;
683  my $text = $self->_convert($converted);
684  $text .= _count_added($self, $formatter->{'container'},
685                Texinfo::Convert::Paragraph::end($formatter->{'container'}));
686  pop @{$self->{'formatters'}};
687  return $text;
688}
689
690sub convert_unfilled($$;$)
691{
692  my ($self, $converted, $conf) = @_;
693  my $formatter = $self->new_formatter('unfilled', $conf);
694  $formatter->{'font_type_stack'}->[-1]->{'monospace'} = 1;
695  push @{$self->{'formatters'}}, $formatter;
696  my $result = $self->_convert($converted);
697  $result .= _count_added($self, $formatter->{'container'},
698                Texinfo::Convert::Paragraph::end($formatter->{'container'}));
699  pop @{$self->{'formatters'}};
700  return $result;
701}
702
703sub count_bytes($$)
704{
705  my ($self, $string) = @_;
706
707  return Texinfo::Common::count_bytes($self, $string,
708                                      $self->{'output_perl_encoding'});
709}
710
711sub _add_text_count($$)
712{
713  my ($self, $text) = @_;
714  if (!$self->{'count_context'}->[-1]->{'pending_text'}) {
715    $self->{'count_context'}->[-1]->{'pending_text'} = '';
716  }
717  $self->{'count_context'}->[-1]->{'pending_text'} .= $text;
718}
719
720sub _add_lines_count($$)
721{
722  my ($self, $lines_count) = @_;
723  $self->{'count_context'}->[-1]->{'lines'} += $lines_count;
724}
725
726# Update $SELF->{'count_context'}->[-1]->{'bytes'} by counting the text that
727# hasn't been counted yet.  It is faster to count the text all together than
728# piece by piece in _add_text_count.
729sub _update_count_context($)
730{
731  my $self = shift;
732  if ($self->{'count_context'}->[-1]->{'pending_text'}) {
733    $self->{'count_context'}->[-1]->{'bytes'} +=
734      Texinfo::Common::count_bytes($self,
735        $self->{'count_context'}->[-1]->{'pending_text'},
736        $self->{'output_perl_encoding'});
737    $self->{'count_context'}->[-1]->{'pending_text'} = '';
738  }
739}
740
741# Save the line and byte offset of $ROOT.
742sub _add_location($$)
743{
744  my ($self, $root) = @_;
745  my $location = { 'lines' => $self->{'count_context'}->[-1]->{'lines'} };
746  push @{$self->{'count_context'}->[-1]->{'locations'}}, $location;
747  if (!($root->{'extra'} and $root->{'extra'}->{'index_entry'})) {
748    _update_count_context($self);
749    $location->{'bytes'} = $self->{'count_context'}->[-1]->{'bytes'};
750    $location->{'root'} = $root;
751  } else {
752    $location->{'index_entry'} = $root;
753  }
754  return $location;
755}
756
757sub _add_image($$$$;$)
758{
759  my ($self, $root, $lines_count, $image_width, $no_align) = @_;
760
761  push @{$self->{'count_context'}->[-1]->{'images'}}, {
762    'lines' => $self->{'count_context'}->[-1]->{'lines'},
763    'lines_count' => $lines_count,
764    'image_width'  => $image_width,
765    'no_align' =>  $no_align,
766    # may be used for debugging?
767    #'_ref' => $root,
768  };
769}
770
771sub _count_added($$$)
772{
773  my ($self, $container, $text) = @_;
774
775  my $count_context = $self->{'count_context'}->[-1];
776  $count_context->{'lines'}
777    += Texinfo::Convert::Paragraph::end_line_count($container);
778
779  if (!defined $count_context->{'pending_text'}) {
780    $count_context->{'pending_text'} = '';
781  }
782  $count_context->{'pending_text'} .= $text;
783  return $text;
784}
785
786sub _update_locations_counts($$)
787{
788  my ($self, $locations) = @_;
789
790  _update_count_context($self);
791  foreach my $location (@$locations) {
792    $location->{'bytes'} += $self->{'count_context'}->[-1]->{'bytes'}
793       if (defined($location->{'bytes'}));
794    $location->{'lines'} += $self->{'count_context'}->[-1]->{'lines'}
795      if (defined($location->{'lines'}));
796  }
797}
798
799sub _add_newline_if_needed($) {
800  my $self = shift;
801  if (defined($self->{'empty_lines_count'})
802       and $self->{'empty_lines_count'} == 0) {
803    _add_text_count($self, "\n");
804    _add_lines_count($self, 1);
805    $self->{'empty_lines_count'} = 1;
806    return "\n";
807  }
808  return '';
809}
810
811my $footnote_indent = 3;
812sub _footnotes($;$)
813{
814  my ($self, $element) = @_;
815
816  $element = undef if ($element and $element->{'extra'}->{'no_node'});
817
818  my $result = '';
819  if (scalar(@{$self->{'pending_footnotes'}})) {
820    $result .= _add_newline_if_needed($self);
821    if ($self->get_conf('footnotestyle') eq 'end' or !defined($element)) {
822      my $footnotes_header = "   ---------- Footnotes ----------\n\n";
823      $result .= $footnotes_header;
824      _add_text_count($self, $footnotes_header);
825      _add_lines_count($self, 2);
826      $self->{'empty_lines_count'} = 1;
827    } else {
828
829      my $node_contents = [@{$element->{'extra'}->{'node'}->{'extra'}->{'node_content'}},
830                                     {'text' => '-Footnotes'}];
831      my $footnotes_node = {
832        'cmdname' => 'node',
833        'node_up' => $element->{'extra'}->{'node'},
834        'extra' => {'node_content' => $node_contents }
835      };
836      $result .= $self->_node($footnotes_node);
837      $self->{'node'} = $footnotes_node;
838    }
839    while (@{$self->{'pending_footnotes'}}) {
840      my $footnote = shift (@{$self->{'pending_footnotes'}});
841
842      # If nested within another footnote and footnotestyle is separate,
843      # the element here will be the parent element and not the footnote
844      # element, while the pxref will point to the name with the
845      # footnote node taken into account.  Not really problematic as
846      # nested footnotes are not right.
847      if ($element) {
848        my $node_contents = [@{$element->{'extra'}->{'node'}->{'extra'}->{'node_content'}},
849                    {'text' => "-Footnote-$footnote->{'number'}"}];
850        $self->_add_location({'cmdname' => 'anchor',
851                        'extra' => {'node_content' => $node_contents }
852                       });
853      }
854      # this pushes on 'context', 'formatters', 'format_context',
855      # 'text_element_context' and 'document_context'
856      $self->push_top_formatter('footnote');
857      my $formatted_footnote_number;
858      if ($self->get_conf('NUMBER_FOOTNOTES')) {
859        $formatted_footnote_number = $footnote->{'number'};
860      } else {
861        $formatted_footnote_number = $NO_NUMBER_FOOTNOTE_SYMBOL;
862      }
863      my $footnote_text = ' ' x $footnote_indent
864               . "($formatted_footnote_number) ";
865      $result .= $footnote_text;
866      $self->{'text_element_context'}->[-1]->{'counter'} +=
867         Texinfo::Convert::Unicode::string_width($footnote_text);
868      _add_text_count($self, $footnote_text);
869      $self->{'empty_lines_count'} = 0;
870
871      $result .= $self->_convert($footnote->{'root'}->{'args'}->[0]);
872      $result .= _add_newline_if_needed($self);
873
874      my $old_context = pop @{$self->{'context'}};
875      die if ($old_context ne 'footnote');
876      pop @{$self->{'formatters'}};
877      pop @{$self->{'format_context'}};
878      pop @{$self->{'text_element_context'}};
879      pop @{$self->{'document_context'}};
880    }
881  }
882  $self->{'footnote_index'} = 0;
883
884  return $result;
885}
886
887sub _compute_spaces_align_line($$$;$)
888{
889  my ($line_width, $max_column, $direction, $no_align) = @_;
890
891  my $spaces_prepended;
892  if ($line_width >= $max_column or $no_align) {
893    $spaces_prepended = 0;
894  } elsif ($direction eq 'center') {
895    # if no int we may end up with floats...
896    $spaces_prepended = int(($max_column -1 - $line_width) /2);
897  } else {
898    $spaces_prepended = ($max_column -1 - $line_width);
899  }
900  return $spaces_prepended;
901}
902
903sub _align_lines($$$$$$)
904{
905  my ($self, $text, $max_column, $direction, $locations, $images) = @_;
906
907  my $result = '';
908
909  my $updated_locations = {};
910  if ($locations and @$locations) {
911    foreach my $location (@$locations) {
912      next unless (defined($location->{'bytes'}));
913      push @{$updated_locations->{$location->{'lines'}}}, $location;
914    }
915  }
916  my $images_marks = {};
917  if ($images and @$images) {
918    foreach my $image (@$images) {
919      if ($image->{'lines_count'} > 1) {
920        if (!$images_marks->{$image->{'lines'}}) {
921          $images_marks->{$image->{'lines'}} = $image;
922        }# else {
923        # Happens in Info with the special construct as, in that
924        # case, there are no lines!  So no error...
925        #  $self->_bug_message("more than one image with lines on $image->{'lines'}");
926        # in that case, the $image->{'lines'} is not in sync with the
927        # lines count.  So the second image will be treated as simple text.
928        #}
929      }
930    }
931  }
932
933  my $bytes_count = 0;
934  my $delta_bytes = 0;
935  my $line_index = 0;
936  my $image;
937  my $image_lines_count;
938  my $image_prepended_spaces;
939  foreach my $line (split /^/, $text) {
940    my $line_bytes_begin = 0;
941    my $line_bytes_end = 0;
942    my $removed_line_bytes_end = 0;
943    my $removed_line_bytes_begin = 0;
944
945    my ($new_image, $new_image_prepended_spaces);
946    if ($images_marks->{$line_index}) {
947      $new_image = $images_marks->{$line_index};
948      $image_lines_count = 0;
949      $new_image_prepended_spaces
950       = _compute_spaces_align_line($new_image->{'image_width'}, $max_column,
951                                    $direction, $new_image->{'no_align'});
952      if (!defined($image)) {
953        $image = $new_image;
954        $image_prepended_spaces = $new_image_prepended_spaces;
955        $new_image = undef;
956      }
957    }
958
959    my $orig_line;
960    if (!$image) {
961      my $chomped = chomp($line);
962      # for debugging.
963      $orig_line = $line;
964      $removed_line_bytes_end -= count_bytes($self, $chomped);
965      $line =~ s/^(\s*)//;
966      $removed_line_bytes_begin -= count_bytes($self, $1);
967      $line =~ s/(\s*)$//;
968      $removed_line_bytes_end -= count_bytes($self, $1);
969      my $line_width = Texinfo::Convert::Unicode::string_width($line);
970      if ($line_width == 0) {
971        $result .= "\n";
972        $line_bytes_end += count_bytes($self, "\n");
973        $bytes_count += count_bytes($self, "\n");
974      } else {
975        my $spaces_prepended
976         = _compute_spaces_align_line($line_width, $max_column, $direction);
977        $result .= ' ' x$spaces_prepended . $line ."\n";
978        $line_bytes_begin += count_bytes($self, ' ' x$spaces_prepended);
979        $line_bytes_end += count_bytes($self, "\n");
980        $bytes_count += $line_bytes_begin + $line_bytes_end
981                        + count_bytes($self, $line);
982      }
983    } else {
984      $image_lines_count++;
985      my $prepended_spaces = $image_prepended_spaces;
986      # adjust if there is something else that the image on the first or
987      # last line.  The adjustment is approximate.
988      if (($image_lines_count == 1 or $image_lines_count == $image->{'lines_count'})
989          and Texinfo::Convert::Unicode::string_width($line) > $image->{'image_width'}) {
990        $prepended_spaces
991         -= Texinfo::Convert::Unicode::string_width($line) - $image->{'image_width'};
992        $prepended_spaces = 0 if ($prepended_spaces < 0);
993      }
994      $result .= ' ' x$prepended_spaces . $line;
995      $line_bytes_begin += count_bytes($self, ' ' x$prepended_spaces);
996      $bytes_count += $line_bytes_begin + count_bytes($self, $line);
997      if ($new_image) {
998        $image = $new_image;
999        $image_prepended_spaces = $new_image_prepended_spaces;
1000      } elsif ($image_lines_count == $image->{'lines_count'}) {
1001        $image = undef;
1002        $image_lines_count = undef;
1003        $image_prepended_spaces = undef;
1004      }
1005    }
1006
1007    if ($updated_locations->{$line_index}) {
1008      foreach my $location (@{$updated_locations->{$line_index}}) {
1009        $location->{'bytes'} += $line_bytes_begin + $removed_line_bytes_begin
1010                                + $delta_bytes;
1011      }
1012    }
1013    $delta_bytes += $line_bytes_begin + $line_bytes_end
1014             + $removed_line_bytes_begin + $removed_line_bytes_end;
1015    $line_index++;
1016  }
1017  return ($result, $bytes_count);
1018}
1019
1020sub _align_environment($$$$)
1021{
1022  my ($self, $result, $max, $align) = @_;
1023
1024  _update_count_context($self);
1025  my $counts = pop @{$self->{'count_context'}};
1026  my $bytes_count;
1027  ($result, $bytes_count) = $self->_align_lines($result, $max,
1028                      $align, $counts->{'locations'}, $counts->{'images'});
1029  $self->_update_locations_counts($counts->{'locations'});
1030  $self->{'count_context'}->[-1]->{'bytes'} += $bytes_count;
1031  $self->{'count_context'}->[-1]->{'lines'} += $counts->{'lines'};
1032  push @{$self->{'count_context'}->[-1]->{'locations'}},
1033                       @{$counts->{'locations'}};
1034  return $result;
1035}
1036
1037sub _contents($$$)
1038{
1039  my ($self, $section_root, $contents_or_shortcontents) = @_;
1040
1041  my $contents = 1 if ($contents_or_shortcontents eq 'contents');
1042
1043  # no sections
1044  return ('', 0) if (!$section_root or !$section_root->{'section_childs'});
1045  my $root_level = $section_root->{'section_childs'}->[0]->{'level'};
1046  foreach my $top_section(@{$section_root->{'section_childs'}}) {
1047    $root_level = $top_section->{'level'}
1048      if ($top_section->{'level'} < $root_level);
1049  }
1050
1051  my $result = '';
1052  my $lines_count = 0;
1053  # This is done like that because the tree may not be well formed if
1054  # there is a @part after a @chapter for example.
1055  foreach my $top_section (@{$section_root->{'section_childs'}}) {
1056    my $section = $top_section;
1057 SECTION:
1058    while ($section) {
1059      push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0};
1060      my $section_title_tree;
1061      if (defined($section->{'number'})
1062          and ($self->get_conf('NUMBER_SECTIONS')
1063               or !defined($self->get_conf('NUMBER_SECTIONS')))) {
1064        if ($section->{'cmdname'} eq 'appendix' and $section->{'level'} == 1) {
1065          $section_title_tree = $self->gdt('Appendix {number} {section_title}',
1066                           {'number' => {'text' => $section->{'number'}},
1067                            'section_title'
1068                              => {'contents' => $section->{'args'}->[0]->{'contents'}}});
1069        } else {
1070          $section_title_tree = $self->gdt('{number} {section_title}',
1071                           {'number' => {'text' => $section->{'number'}},
1072                            'section_title'
1073                              => {'contents' => $section->{'args'}->[0]->{'contents'}}});
1074        }
1075      } else {
1076        $section_title_tree = {'contents' => $section->{'args'}->[0]->{'contents'}};
1077      }
1078      my $section_title = $self->convert_line(
1079            {'contents' => [$section_title_tree],
1080             'type' => 'frenchspacing'});
1081      pop @{$self->{'count_context'}};
1082      my $text = $section_title;
1083      chomp ($text);
1084      $text .= "\n";
1085      my $repeat_count = 2 * ($section->{'level'} - ($root_level+1));
1086      ($result .= (' ' x $repeat_count)) if $repeat_count > 0;
1087      $result .= $text;
1088      $lines_count++;
1089      if ($section->{'section_childs'}
1090          and ($contents or $section->{'level'} < $root_level+1)) {
1091        $section = $section->{'section_childs'}->[0];
1092      } elsif ($section->{'section_next'}) {
1093        last if ($section eq $top_section);
1094        $section = $section->{'section_next'};
1095      } else {
1096        last if ($section eq $top_section);
1097        while ($section->{'section_up'}) {
1098          $section = $section->{'section_up'};
1099          last SECTION if ($section eq $top_section);
1100          if ($section->{'section_next'}) {
1101            $section = $section->{'section_next'};
1102            last;
1103          }
1104        }
1105      }
1106    }
1107  }
1108  return ($result, $lines_count);
1109}
1110
1111sub _menu($$)
1112{
1113  my ($self, $menu_command) = @_;
1114
1115  if ($menu_command->{'cmdname'} eq 'menu') {
1116    my $result = "* Menu:\n\n";
1117    _add_text_count($self, $result);
1118    _add_lines_count($self, 2);
1119    if ($self->{'node'}) {
1120      $self->{'seenmenus'}->{$self->{'node'}} = 1;
1121    }
1122    return $result;
1123  } else {
1124    return '';
1125  }
1126}
1127
1128sub _printindex($$)
1129{
1130  my ($self, $printindex) = @_;
1131  return $self->_printindex_formatted($printindex);
1132}
1133
1134sub _normalize_top_node($)
1135{
1136  my $node = shift;
1137  return Texinfo::Common::normalize_top_node_name($node);
1138}
1139
1140# convert and cache a node name.  $NODE is a node element.
1141sub _node_line($$)
1142{
1143  my ($self, $node) = @_;
1144  if (!$self->{'node_lines_text'}->{$node}) {
1145    my $node_text = {'type' => '_code',
1146              'contents' => $node->{'extra'}->{'node_content'}};
1147    push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0};
1148    $self->{'node_lines_text'}->{$node}->{'text'}
1149       = _normalize_top_node($self->convert_line($node_text,
1150                                                 {'suppress_styles' => 1}));
1151    _update_count_context($self);
1152    my $end_context = pop @{$self->{'count_context'}};
1153    $self->{'node_lines_text'}->{$node}->{'count'}
1154      = $end_context->{'bytes'};
1155  }
1156  return ($self->{'node_lines_text'}->{$node}->{'text'},
1157          $self->{'node_lines_text'}->{$node}->{'count'});
1158}
1159
1160my $index_length_to_node = 41;
1161
1162sub _printindex_formatted($$;$)
1163{
1164  my ($self, $printindex, $in_info) = @_;
1165
1166  my $index_name;
1167
1168  if ($printindex->{'extra'} and $printindex->{'extra'}->{'misc_args'}
1169      and defined($printindex->{'extra'}->{'misc_args'}->[0])) {
1170    $index_name = $printindex->{'extra'}->{'misc_args'}->[0];
1171  } else {
1172    return '';
1173  }
1174
1175  # this is not redone for each index, only once
1176  if (!defined($self->{'index_entries'}) and $self->{'parser'}) {
1177
1178    my $index_names = $self->{'parser'}->indices_information();
1179    my $merged_index_entries
1180      = Texinfo::Structuring::merge_indices($index_names);
1181    $self->{'index_entries'}
1182      = Texinfo::Structuring::sort_indices($self->{'parser'},
1183                                $merged_index_entries, $index_names);
1184    $self->{'index_names'} = $index_names;
1185  }
1186  if (!$self->{'index_entries'} or !$self->{'index_entries'}->{$index_name}
1187      or ! @{$self->{'index_entries'}->{$index_name}}) {
1188    return '';
1189  }
1190
1191  my $result = '';
1192  $result .= _add_newline_if_needed($self);
1193  if ($in_info) {
1194    my $info_printindex_magic = "\x{00}\x{08}[index\x{00}\x{08}]\n";
1195    $result .= $info_printindex_magic;
1196    _add_text_count($self, $info_printindex_magic);
1197    _add_lines_count($self, 1);
1198  }
1199  my $heading = "* Menu:\n\n";
1200
1201  $result .= $heading;
1202  _add_text_count($self, $heading);
1203  _add_lines_count($self, 2);
1204
1205  # first determine the line numbers for the spacing of their formatting
1206  my %line_nrs;
1207  my %entry_nodes;
1208  my $max_index_line_nr_string_length = 0;
1209  my %ignored_entries;
1210  foreach my $entry (@{$self->{'index_entries'}->{$index_name}}) {
1211    my $line_nr;
1212
1213    if (defined ($self->{'index_entries_line_location'}->{$entry->{'command'}})) {
1214      $line_nr = $self->{'index_entries_line_location'}->{$entry->{'command'}}->{'lines'};
1215      # ignore index entries in special regions that haven't been seen
1216    } elsif ($entry->{'region'}) {
1217      $ignored_entries{$entry} = 1;
1218      next;
1219    }
1220
1221    my $node;
1222    # priority given to the location determined dynamically as the
1223    # index entry may be in footnote.
1224    if (defined($self->{'index_entries_line_location'}->{$entry->{'command'}}->{'node'})) {
1225      $node = $self->{'index_entries_line_location'}->{$entry->{'command'}}->{'node'};
1226    } elsif (defined($entry->{'node'})) {
1227      $node = $entry->{'node'};
1228    }
1229    $entry_nodes{$entry} = $node;
1230    if (!defined($node)) {
1231      $line_nr = 0;
1232    } elsif($in_info) {
1233      $line_nr = 3 if (defined($line_nr) and $line_nr < 3);
1234      $line_nr = 4 if (!defined($line_nr));
1235    } else {
1236      $line_nr = 0 if (!defined($line_nr));
1237    }
1238    my $index_line_nr_string_length =
1239      Texinfo::Convert::Unicode::string_width($line_nr);
1240    $max_index_line_nr_string_length = $index_line_nr_string_length
1241     if ($max_index_line_nr_string_length < $index_line_nr_string_length);
1242    $line_nrs{$entry} = $line_nr;
1243  }
1244
1245  # this is used to count entries that are the same
1246  my %entry_counts = ();
1247
1248  foreach my $entry (@{$self->{'index_entries'}->{$index_name}}) {
1249    next if ($ignored_entries{$entry});
1250    my $entry_tree = {'contents' => $entry->{'content'}};
1251    if ($entry->{'in_code'}) {
1252      $entry_tree->{'type'} = '_code';
1253    } else {
1254      $entry_tree->{'type'} = 'frenchspacing';
1255    }
1256    my $entry_text = '';
1257
1258    my $formatter = $self->new_formatter('line',
1259                                   {'indent' => 0, 'suppress_styles' => 1});
1260    push @{$self->{'formatters'}}, $formatter;
1261    $entry_text = $self->_convert($entry_tree);
1262    $entry_text .= $self->convert_index_subentries($entry);
1263    $entry_text .= _count_added($self, $formatter->{'container'},
1264                  Texinfo::Convert::Paragraph::end($formatter->{'container'}));
1265    pop @{$self->{'formatters'}};
1266
1267    next if ($entry_text !~ /\S/);
1268
1269    # FIXME protect instead
1270    if ($entry_text =~ /:/ and $self->get_conf('INDEX_SPECIAL_CHARS_WARNING')) {
1271      $self->line_warn (sprintf(__("Index entry in \@%s with : produces invalid Info: %s"),
1272                                 $entry->{'index_at_command'},
1273          Texinfo::Convert::Texinfo::convert($entry_tree)),
1274                        $entry->{'command'}->{'line_nr'});
1275    }
1276
1277    my $entry_nr = '';
1278    if (!defined($entry_counts{$entry_text})) {
1279      $entry_counts{$entry_text} = 0;
1280    } else {
1281      $entry_counts{$entry_text}++;
1282      $entry_nr = ' <'.$entry_counts{$entry_text}.'>';
1283      _add_text_count($self, $entry_nr);
1284    }
1285    my $entry_line = "* $entry_text${entry_nr}: ";
1286    _add_text_count($self, "* ".": ");
1287    #_add_text_count($self, $entry_line);
1288
1289    my $line_width = Texinfo::Convert::Unicode::string_width($entry_line);
1290    my $entry_line_addition = '';
1291    if ($line_width < $index_length_to_node) {
1292      my $spaces = ' ' x ($index_length_to_node - $line_width);
1293      $entry_line_addition .= $spaces;
1294      _add_text_count($self, $spaces);
1295    }
1296    my $node = $entry_nodes{$entry};
1297
1298    if (!defined($node)) {
1299      # cache the transformation to text and byte counting, as
1300      # it is likeky that there is more than one such entry
1301      if (!$self->{'outside_of_any_node_text'}) {
1302        push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0};
1303        my $node_text = $self->gdt('(outside of any node)');
1304        $self->{'outside_of_any_node_text'}->{'text'}
1305          = $self->convert_line($node_text);
1306        _update_count_context($self);
1307        my $end_context = pop @{$self->{'count_context'}};
1308        $self->{'outside_of_any_node_text'}->{'count'}
1309          = $end_context->{'bytes'};
1310      }
1311      $entry_line_addition .= $self->{'outside_of_any_node_text'}->{'text'};
1312      $self->{'count_context'}->[-1]->{'bytes'}
1313            += $self->{'outside_of_any_node_text'}->{'count'};
1314      # FIXME when outside of sectioning commands this message was already
1315      # done by the Parser.
1316      # Warn, only once.
1317      if (!$self->{'index_entries_no_node'}->{$entry}) {
1318        $self->line_warn(sprintf(__("entry for index `%s' outside of any node"),
1319                                 $index_name), $entry->{'command'}->{'line_nr'});
1320        $self->{'index_entries_no_node'}->{$entry} = 1;
1321      }
1322    } else {
1323      my ($node_line, $byte_count) = $self->_node_line($node);
1324      $entry_line_addition .= $node_line;
1325      $self->{'count_context'}->[-1]->{'bytes'} += $byte_count;
1326    }
1327    $entry_line_addition .= '.';
1328    _add_text_count($self, '.');
1329
1330    $entry_line .= $entry_line_addition;
1331    $result .= $entry_line;
1332
1333    my $line_nr = $line_nrs{$entry};
1334    my $line_nr_spaces = sprintf("%${max_index_line_nr_string_length}d", $line_nr);
1335    my $line_part = "(line ${line_nr_spaces})";
1336    $line_width += Texinfo::Convert::Unicode::string_width($entry_line_addition);
1337    my $line_part_width = Texinfo::Convert::Unicode::string_width($line_part);
1338    if ($line_width + $line_part_width +1 > $self->{'fillcolumn'}) {
1339      $line_part = "\n" . ' ' x ($self->{'fillcolumn'} - $line_part_width)
1340           . "$line_part\n";
1341      _add_lines_count($self, 1);
1342    } else {
1343      $line_part
1344        = ' ' x ($self->{'fillcolumn'} - $line_part_width - $line_width)
1345           . "$line_part\n";
1346    }
1347    _add_lines_count($self, 1);
1348    _add_text_count($self, $line_part);
1349    $result .= $line_part;
1350  }
1351
1352  $result .= "\n";
1353  _add_text_count($self, "\n");
1354  _add_lines_count($self, 1);
1355
1356  return $result;
1357}
1358
1359
1360sub _node($$)
1361{
1362  my $self = shift;
1363  my $node = shift;
1364
1365  return '';
1366}
1367
1368# no error in plaintext
1369sub _error_outside_of_any_node($$)
1370{
1371  my $self = shift;
1372  my $root = shift;
1373}
1374
1375sub _anchor($$)
1376{
1377  my $self = shift;
1378  my $anchor = shift;
1379
1380  if (!($self->{'multiple_pass'} or $self->{'in_copying_header'})) {
1381    $self->_add_location($anchor);
1382    $self->_error_outside_of_any_node($anchor);
1383  }
1384  return '';
1385}
1386
1387my $listoffloat_entry_length = 41;
1388my $listoffloat_append = '...';
1389
1390sub ensure_end_of_line($$)
1391{
1392  my ($self, $text) = @_;
1393
1394  my $chomped = chomp ($text);
1395  if ($chomped) {
1396    $self->{'count_context'}->[-1]->{'bytes'} -= count_bytes($self, $chomped);
1397    $self->{'count_context'}->[-1]->{'lines'} -= 1;
1398  }
1399  $text .= "\n";
1400  $self->{'text_element_context'}->[-1]->{'counter'} = 0;
1401  _add_text_count($self, "\n");
1402  _add_lines_count($self, 1);
1403  return $text;
1404}
1405
1406sub _image_text($$$)
1407{
1408  my ($self, $root, $basefile) = @_;
1409
1410  my $txt_file = $self->Texinfo::Common::locate_include_file($basefile.'.txt');
1411  if (!defined($txt_file)) {
1412    return undef;
1413  } else {
1414    my $filehandle = do { local *FH };
1415    if (open ($filehandle, $txt_file)) {
1416      my $enc = $root->{'extra'}->{'input_perl_encoding'};
1417      binmode($filehandle, ":encoding($enc)")
1418        if ($enc);
1419      my $result = '';
1420      my $max_width = 0;
1421      while (<$filehandle>) {
1422        my $width = Texinfo::Convert::Unicode::string_width($_);
1423        if ($width > $max_width) {
1424          $max_width = $width;
1425        }
1426        $result .= $_;
1427      }
1428      # remove last end of line
1429      chomp ($result);
1430      if (!close ($filehandle)) {
1431        $self->document_warn(sprintf(__("error on closing image text file %s: %s"),
1432                                     $txt_file, $!));
1433      }
1434      return ($result, $max_width);
1435    } else {
1436      $self->line_warn(sprintf(__("\@image file `%s' unreadable: %s"),
1437                               $txt_file, $!), $root->{'line_nr'});
1438    }
1439  }
1440  return undef;
1441}
1442
1443sub _image_formatted_text($$$$)
1444{
1445  my ($self, $root, $basefile, $text) = @_;
1446
1447  my $result;
1448  if (defined($text)) {
1449    $result = $text;
1450  } elsif (defined($root->{'args'}->[3])
1451      and @{$root->{'args'}->[3]->{'contents'}}) {
1452    $result = '[' .Texinfo::Convert::Text::convert(
1453      {'contents' => $root->{'args'}->[3]->{'contents'}},
1454      $self->{'convert_text_options'}) .']';
1455  } else {
1456    $self->line_warn(sprintf(__(
1457                    "could not find \@image file `%s.txt' nor alternate text"),
1458                             $basefile), $root->{'line_nr'});
1459    $result = '['.$basefile.']';
1460  }
1461  return $result;
1462}
1463
1464sub _image($$)
1465{
1466  my ($self, $root) = @_;
1467
1468  if (defined($root->{'args'}->[0])
1469        and @{$root->{'args'}->[0]->{'contents'}}) {
1470    my $basefile = Texinfo::Convert::Text::convert(
1471     {'contents' => $root->{'args'}->[0]->{'contents'}},
1472     {'code' => 1, %{$self->{'convert_text_options'}}});
1473    my ($text, $width) = $self->_image_text($root, $basefile);
1474    my $result = $self->_image_formatted_text($root, $basefile, $text);
1475    my $lines_count = ($result =~ tr/\n/\n/);
1476    if (!defined($width)) {
1477      $width = Texinfo::Convert::Unicode::string_width($result);
1478    }
1479    # the last line is part of the image but do not have a new line,
1480    # so 1 is added to $lines_count to have the number of lines of
1481    # the image
1482    $self->_add_image($root, $lines_count+1, $width);
1483    return ($result, $lines_count);
1484  }
1485  return ('', 0);
1486}
1487
1488sub _get_form_feeds($)
1489{
1490  my $form_feeds = shift;
1491  $form_feeds =~ s/^[^\f]*//;
1492  $form_feeds =~ s/[^\f]$//;
1493  return $form_feeds;
1494}
1495
1496sub _convert($$);
1497
1498# Convert the Texinfo tree under $ROOT to plain text.
1499sub _convert($$)
1500{
1501  my ($self, $root) = @_;
1502
1503  my $formatter = $self->{'formatters'}->[-1];
1504
1505  if (($root->{'type'} and $self->{'ignored_types'}->{$root->{'type'}})
1506       or ($root->{'cmdname'}
1507            and ($self->{'ignored_commands'}->{$root->{'cmdname'}}
1508                 or ($inline_commands{$root->{'cmdname'}}
1509                     and $root->{'cmdname'} ne 'inlinefmtifelse'
1510                     and (($inline_format_commands{$root->{'cmdname'}}
1511                          and (!$root->{'extra'}->{'format'}
1512                               or !$self->{'expanded_formats_hash'}->{$root->{'extra'}->{'format'}}))
1513                         or (!$inline_format_commands{$root->{'cmdname'}}
1514                             and !defined($root->{'extra'}->{'expand_index'}))))))) {
1515    return '';
1516  }
1517  my $result = '';
1518
1519  my $type = $root->{'type'};
1520  my $command = $root->{'cmdname'};
1521
1522  # in ignorable spaces, keep only form feeds.
1523  if ($type and $self->{'ignorable_space_types'}->{$type}
1524      and ($type ne 'empty_spaces_before_paragraph'
1525           or $self->get_conf('paragraphindent') ne 'asis')) {
1526    if ($root->{'text'} =~ /\f/) {
1527      $result = _get_form_feeds($root->{'text'});
1528    }
1529    _add_text_count($self, $result);
1530    return $result;
1531  }
1532
1533  # First handle empty lines. This has to be done before the handling
1534  # of text below to be sure that an empty line is always processed
1535  # especially
1536  if ($type and ($type eq 'empty_line'
1537                           or $type eq 'after_description_line')) {
1538    delete $self->{'text_element_context'}->[-1]->{'counter'};
1539    $self->{'empty_lines_count'}++;
1540    if ($self->{'empty_lines_count'} <= 1
1541        or $self->{'preformatted_context_commands'}->{$self->{'context'}->[-1]}) {
1542      $result = "";
1543      if ($root->{'text'} =~ /\f/) {
1544        $result .= _get_form_feeds($root->{'text'});
1545        _add_text_count($self, $result);
1546      }
1547      $result .= _count_added($self, $formatter->{'container'},
1548                add_text($formatter->{'container'}, "\n"));
1549      return $result;
1550    } else {
1551      return '';
1552    }
1553  }
1554
1555  # process text
1556  if (defined($root->{'text'})) {
1557    if (!$type or $type ne 'untranslated') {
1558      if (!$formatter->{'_top_formatter'}) {
1559        if ($type and ($type eq 'raw'
1560                                 or $type eq 'last_raw_newline')) {
1561          $result = _count_added($self, $formatter->{'container'},
1562                      add_next($formatter->{'container'}, $root->{'text'}));
1563        } else {
1564          my $text = _process_text($self, $root, $formatter);
1565          $result = _count_added($self, $formatter->{'container'},
1566                      add_text ($formatter->{'container'}, $text));
1567        }
1568        return $result;
1569      # the following is only possible if paragraphindent is set to asis
1570      } elsif ($type and $type eq 'empty_spaces_before_paragraph') {
1571        _add_text_count($self, $root->{'text'});
1572        return $root->{'text'};
1573      # ignore text outside of any format, but warn if ignored text not empty
1574      } elsif ($root->{'text'} =~ /\S/) {
1575        $self->_bug_message("ignored text not empty `$root->{'text'}'", $root);
1576        return '';
1577      } else {
1578        # miscellaneous top-level whitespace - possibly after an @image
1579        return _count_added($self, $formatter->{'container'},
1580                  add_text($formatter->{'container'}, $root->{'text'}));
1581      }
1582    } else {
1583      my $tree = $self->gdt($root->{'text'});
1584      my $converted = _convert($self, $tree);
1585      return $converted;
1586    }
1587  }
1588
1589  if ($root->{'extra'}) {
1590    if ($root->{'extra'}->{'missing_argument'}
1591             and (!$root->{'contents'} or !@{$root->{'contents'}})) {
1592      return '';
1593    }
1594  }
1595
1596  if ($root->{'extra'} and $root->{'extra'}->{'index_entry'}
1597      and !$self->{'multiple_pass'} and !$self->{'in_copying_header'}) {
1598    my $location = $self->_add_location($root);
1599    # remove a 'lines' from $location if at the very end of a node
1600    # since it will lead to the next node otherwise.
1601    if ($command and $command =~ /index/) {
1602      my $following_not_empty;
1603      my @parents = @{$self->{'current_roots'}};
1604      my @parent_contents = @{$self->{'current_contents'}};
1605      while (@parents) {
1606        my $parent = pop @parents;
1607        my $parent_content = pop @parent_contents;
1608        if ($parent->{'type'} and $parent->{'type'} eq 'paragraph') {
1609          $following_not_empty = 1;
1610          last;
1611        }
1612        foreach my $following_content (@$parent_content) {
1613          unless (($following_content->{'type'}
1614                and ($following_content->{'type'} eq 'empty_line'
1615                    or $ignorable_types{$following_content->{'type'}}))
1616              or ($following_content->{'cmdname'}
1617                  and ($following_content->{'cmdname'} eq 'c'
1618                       or $following_content->{'cmdname'} eq 'comment'))) {
1619            $following_not_empty = 1;
1620            last;
1621          }
1622        }
1623        last if $following_not_empty;
1624        if ($parent->{'cmdname'} and $root_commands{$parent->{'cmdname'}}) {
1625          last;
1626        }
1627      }
1628      if (! $following_not_empty) {
1629        $location->{'lines'}--;
1630      }
1631    }
1632    # this covers the special case for index entry not associated with a
1633    # node but seen.  this will be an index entry in @copying,
1634    # in @insertcopying.
1635    # This also covers the case of an index entry in a node added by a
1636    # @footnote with footnotestyle separate.
1637    if ($self->{'node'}) {
1638      $location->{'node'} = $self->{'node'};
1639    }
1640    $self->{'index_entries_line_location'}->{$root} = $location;
1641  }
1642
1643  my $cell;
1644  my $preformatted;
1645  if ($command) {
1646    my $unknown_command;
1647    if (defined($no_brace_commands{$command})) {
1648      if ($command eq ':') {
1649        remove_end_sentence($formatter->{'container'});
1650        return '';
1651      } elsif ($command eq '*') {
1652        $result = _count_added($self, $formatter->{'container'},
1653                              add_pending_word($formatter->{'container'}));
1654        $result .= _count_added($self, $formatter->{'container'},
1655                              end_line($formatter->{'container'}));
1656      } elsif ($command eq '.' or $command eq '?' or $command eq '!') {
1657        $result .= _count_added($self, $formatter->{'container'},
1658            add_next($formatter->{'container'}, $command));
1659        add_end_sentence($formatter->{'container'}, 1);
1660      } elsif ($command eq ' ' or $command eq "\n" or $command eq "\t") {
1661        $result .= _count_added($self, $formatter->{'container'},
1662            add_next($formatter->{'container'}, $no_brace_commands{$command}));
1663      } else {
1664        $result .= _count_added($self, $formatter->{'container'},
1665            add_text($formatter->{'container'}, $no_brace_commands{$command}));
1666      }
1667      return $result;
1668    } elsif ($command eq 'today') {
1669      my $today = $self->Texinfo::Common::expand_today();
1670      unshift @{$self->{'current_contents'}->[-1]}, $today;
1671    } elsif (exists($brace_no_arg_commands{$command})) {
1672      my $text;
1673
1674      if ($command eq 'dots' or $command eq 'enddots') {
1675        # Don't use Unicode ellipsis character.
1676        $text = '...';
1677      } else {
1678        $text = Texinfo::Convert::Text::brace_no_arg_command($root,
1679                                           $self->{'convert_text_options'});
1680      }
1681
1682      # @AA{} should suppress an end sentence, @aa{} shouldn't.  This
1683      # is the case whether we are in @sc or not.
1684      if ($formatter->{'upper_case'}
1685          and $letter_no_arg_commands{$command}) {
1686        $text = _protect_sentence_ends($text);
1687        $text = uc($text);
1688      }
1689
1690      if ($punctuation_no_arg_commands{$command}) {
1691        $result .= _count_added($self, $formatter->{'container'},
1692                    add_next($formatter->{'container'}, $text));
1693        add_end_sentence($formatter->{'container'}, 1);
1694      } elsif ($command eq 'tie') {
1695        $formatter->{'w'}++;
1696        set_space_protection($formatter->{'container'}, 1, undef)
1697          if ($formatter->{'w'} == 1);
1698        $result .= _count_added($self, $formatter->{'container'},
1699                       add_text($formatter->{'container'}, $text));
1700        $formatter->{'w'}--;
1701        set_space_protection($formatter->{'container'}, 0, undef)
1702          if ($formatter->{'w'} == 0);
1703      } else {
1704        $result .= _count_added($self, $formatter->{'container'},
1705                       add_text($formatter->{'container'}, $text));
1706
1707        # This is to have @TeX{}, for example, not to prevent end sentences.
1708        if (!$letter_no_arg_commands{$command}) {
1709          allow_end_sentence($formatter->{'container'});
1710        }
1711
1712        if ($command eq 'dots') {
1713          remove_end_sentence($formatter->{'container'});
1714        }
1715      }
1716      if ($formatter->{'var'}
1717          or $formatter->{'font_type_stack'}->[-1]->{'monospace'}) {
1718        allow_end_sentence($formatter->{'container'});
1719      }
1720      return $result;
1721    # commands with braces
1722    } elsif ($accent_commands{$command}) {
1723      my $encoding;
1724      if ($self->{'enable_encoding'}) {
1725        $encoding = $self->{'output_encoding_name'};
1726      }
1727      my $sc;
1728      if ($formatter->{'upper_case'}) {
1729        $sc = 1;
1730      }
1731      my $accented_text
1732         = Texinfo::Convert::Text::text_accents($root, $encoding, $sc);
1733      $result .= _count_added($self, $formatter->{'container'},
1734         add_text($formatter->{'container'}, $accented_text));
1735
1736      my $accented_text_original;
1737      if ($formatter->{'upper_case'}) {
1738        $accented_text_original
1739         = Texinfo::Convert::Text::text_accents($root, $encoding);
1740      }
1741
1742      if ($accented_text_original
1743            and $accented_text_original !~ /[[:upper:]]/
1744          or $formatter->{'var'}
1745          or $formatter->{'font_type_stack'}->[-1]->{'monospace'}) {
1746        allow_end_sentence($formatter->{'container'});
1747      }
1748
1749      # in case the text added ends with punctuation.
1750      # If the text is empty (likely because of an error) previous
1751      # punctuation will be cancelled, we don't want that.
1752      remove_end_sentence($formatter->{'container'})
1753        if ($accented_text ne '');
1754      return $result;
1755    } elsif ($self->{'style_map'}->{$command}
1756         or ($root->{'type'} and $root->{'type'} eq 'definfoenclose_command')) {
1757      if ($code_style_commands{$command}) {
1758        if (!$formatter->{'font_type_stack'}->[-1]->{'monospace'}) {
1759          push @{$formatter->{'font_type_stack'}}, {'monospace' => 1};
1760        } else {
1761          $formatter->{'font_type_stack'}->[-1]->{'monospace'}++;
1762        }
1763      } elsif ($regular_font_style_commands{$command}) {
1764        if ($formatter->{'font_type_stack'}->[-1]->{'monospace'}) {
1765          push @{$formatter->{'font_type_stack'}}, {'monospace' => 0,
1766                                                    'normal' => 1};
1767        } elsif ($formatter->{'font_type_stack'}->[-1]->{'normal'}) {
1768          $formatter->{'font_type_stack'}->[-1]->{'normal'}++;
1769        }
1770      }
1771      if ($no_punctation_munging_commands{$command}) {
1772        push @{$formatter->{'frenchspacing_stack'}}, 'on';
1773        set_space_protection($formatter->{'container'}, undef,
1774          undef,undef,1);
1775      }
1776      if ($upper_case_commands{$command}) {
1777        $formatter->{'upper_case'}++;
1778        $formatter->{'var'}++ if ($command eq 'var');
1779      }
1780      if ($command eq 'w') {
1781        $formatter->{'w'}++;
1782        set_space_protection($formatter->{'container'}, 1,undef)
1783          if ($formatter->{'w'} == 1);
1784      }
1785      my ($text_before, $text_after);
1786      if ($root->{'type'} and $root->{'type'} eq 'definfoenclose_command') {
1787        $text_before = $root->{'extra'}->{'begin'};
1788        $text_after = $root->{'extra'}->{'end'};
1789      } elsif ($non_quoted_commands_when_nested{$command}
1790            and $formatter->{'font_type_stack'}->[-1]->{'code_command'}) {
1791        $text_before = '';
1792        $text_after = '';
1793      } elsif ($formatter->{'suppress_styles'}
1794               and !$index_style_commands{$command}) {
1795        $text_before = '';
1796        $text_after = '';
1797      } else {
1798        $text_before = $self->{'style_map'}->{$command}->[0];
1799        $text_after = $self->{'style_map'}->{$command}->[1];
1800      }
1801      # do this after determining $text_before/$text_after such that it
1802      # doesn't impact the current command, but only commands nested within
1803      if ($non_quoted_commands_when_nested{$command}) {
1804        $formatter->{'font_type_stack'}->[-1]->{'code_command'}++;
1805      }
1806      $result .= _count_added($self, $formatter->{'container'},
1807               add_next($formatter->{'container'}, $text_before, 1))
1808         if ($text_before ne '');
1809      if ($root->{'args'}) {
1810        $result .= _convert($self, $root->{'args'}->[0]);
1811        if ($command eq 'strong'
1812             and scalar (@{$root->{'args'}->[0]->{'contents'}})
1813             and $root->{'args'}->[0]->{'contents'}->[0]->{'text'}
1814             and $root->{'args'}->[0]->{'contents'}->[0]->{'text'} =~ /^Note\s/i
1815             and $self->{'output_format'}
1816             and $self->{'output_format'} eq 'info') {
1817          $self->line_warn(__(
1818    "\@strong{Note...} produces a spurious cross-reference in Info; reword to avoid that"),
1819                           $root->{'line_nr'});
1820        }
1821      }
1822      $result .= _count_added($self, $formatter->{'container'},
1823               add_next($formatter->{'container'}, $text_after, 1))
1824         if ($text_after ne '');
1825      if ($command eq 'w') {
1826        $formatter->{'w'}--;
1827        set_space_protection($formatter->{'container'},0,undef)
1828          if ($formatter->{'w'} == 0);
1829      }
1830      if ($code_style_commands{$command}) {
1831        $formatter->{'font_type_stack'}->[-1]->{'monospace'}--;
1832        allow_end_sentence($formatter->{'container'});
1833        pop @{$formatter->{'font_type_stack'}}
1834          if !$formatter->{'font_type_stack'}->[-1]->{'monospace'};
1835      } elsif ($regular_font_style_commands{$command}) {
1836        if ($formatter->{'font_type_stack'}->[-1]->{'normal'}) {
1837          $formatter->{'font_type_stack'}->[-1]->{'normal'}--;
1838          pop @{$formatter->{'font_type_stack'}}
1839            if !$formatter->{'font_type_stack'}->[-1]->{'normal'};
1840        }
1841      }
1842      if ($non_quoted_commands_when_nested{$command}) {
1843        #$formatter->{'code_command'}--;
1844        $formatter->{'font_type_stack'}->[-1]->{'code_command'}--;
1845      }
1846      if ($no_punctation_munging_commands{$command}) {
1847        pop @{$formatter->{'frenchspacing_stack'}};
1848        my $frenchspacing = 0;
1849        $frenchspacing = 1 if ($formatter->{'frenchspacing_stack'}->[-1] eq 'on');
1850        set_space_protection($formatter->{'container'}, undef,
1851          undef, undef, $frenchspacing);
1852      }
1853      if ($upper_case_commands{$command}) {
1854        $formatter->{'upper_case'}--;
1855        if ($command eq 'var') {
1856          $formatter->{'var'}--;
1857          # Allow a following full stop to terminate a sentence.
1858          allow_end_sentence($formatter->{'container'});
1859        }
1860      }
1861      return $result;
1862    } elsif ($command eq 'image') {
1863      $result = _count_added($self, $formatter->{'container'},
1864                   add_pending_word($formatter->{'container'}, 1));
1865      # add an empty word so that following spaces aren't lost
1866      add_next($formatter->{'container'},'');
1867      my ($image, $lines_count) = $self->_image($root);
1868      _add_lines_count($self, $lines_count);
1869      _add_text_count($self, $image);
1870      if ($image ne '' and $formatter->{'type'} ne 'paragraph') {
1871        $self->{'empty_lines_count'} = 0;
1872      }
1873      $result .= $image;
1874      return $result;
1875    } elsif ($command eq 'email') {
1876      # nothing is output for email, instead the command is substituted.
1877      my @email_contents;
1878      if ($root->{'args'}) {
1879        my $name;
1880        my $email;
1881        if (scalar (@{$root->{'args'}}) == 2
1882            and defined($root->{'args'}->[1])
1883            and @{$root->{'args'}->[1]->{'contents'}}) {
1884          $name = $root->{'args'}->[1]->{'contents'};
1885        }
1886        if (defined($root->{'args'}->[0])
1887            and @{$root->{'args'}->[0]->{'contents'}}) {
1888          $email = $root->{'args'}->[0]->{'contents'};
1889        }
1890        my $prepended;
1891        if ($name and $email) {
1892          $prepended = $self->gdt('{name} @url{{email}}',
1893                           {'name' => $name, 'email' => $email});
1894        } elsif ($email) {
1895          $prepended = $self->gdt('@url{{email}}',
1896                           {'email' => $email});
1897        } elsif ($name) {
1898          $prepended = {'contents' => $name};
1899        } else {
1900          return '';
1901        }
1902        unshift @{$self->{'current_contents'}->[-1]}, $prepended;
1903      }
1904      return '';
1905    } elsif ($command eq 'uref' or $command eq 'url') {
1906      if ($root->{'args'}) {
1907        if (scalar(@{$root->{'args'}}) == 3
1908             and defined($root->{'args'}->[2])
1909             and @{$root->{'args'}->[2]->{'contents'}}) {
1910          unshift @{$self->{'current_contents'}->[-1]},
1911            {'contents' => $root->{'args'}->[2]->{'contents'}};
1912        } elsif (@{$root->{'args'}->[0]->{'contents'}}) {
1913          # no mangling of --- and similar in url.
1914          my $url = {'type' => '_code',
1915              'contents' => $root->{'args'}->[0]->{'contents'}};
1916          if (scalar(@{$root->{'args'}}) == 2
1917             and defined($root->{'args'}->[1])
1918             and @{$root->{'args'}->[1]->{'contents'}}) {
1919            my $prepended = $self->gdt('{text} ({url})',
1920                 {'text' => $root->{'args'}->[1]->{'contents'},
1921                  'url' => $url });
1922            unshift @{$self->{'current_contents'}->[-1]}, $prepended;
1923          } else {
1924            my $prepended = $self->gdt('@t{<{url}>}',
1925                                        {'url' => $url});
1926            unshift @{$self->{'current_contents'}->[-1]}, $prepended
1927          }
1928        } elsif (scalar(@{$root->{'args'}}) == 2
1929                 and defined($root->{'args'}->[1])
1930                 and @{$root->{'args'}->[1]->{'contents'}}) {
1931          unshift @{$self->{'current_contents'}->[-1]},
1932            {'contents' => $root->{'args'}->[1]->{'contents'}};
1933        }
1934      }
1935      return '';
1936    } elsif ($command eq 'footnote') {
1937      $self->{'footnote_index'}++ unless ($self->{'multiple_pass'});
1938      my $formatted_footnote_number;
1939      if ($self->get_conf('NUMBER_FOOTNOTES')) {
1940        $formatted_footnote_number = $self->{'footnote_index'};
1941      } else {
1942        $formatted_footnote_number = $NO_NUMBER_FOOTNOTE_SYMBOL;
1943      }
1944      push @{$self->{'pending_footnotes'}}, {'root' => $root,
1945                                    'number' => $self->{'footnote_index'}}
1946          unless ($self->{'multiple_pass'});
1947      if (!$self->{'in_copying_header'}) {
1948        $self->_error_outside_of_any_node($root);
1949      }
1950      $result .= _count_added($self, $formatter->{'container'},
1951           add_next($formatter->{'container'},
1952                    "($formatted_footnote_number)", 1));
1953      if ($self->get_conf('footnotestyle') eq 'separate' and $self->{'node'}) {
1954        $result .= _convert($self, {'contents' =>
1955         [{'text' => ' ('},
1956          {'cmdname' => 'pxref',
1957           'args' => [
1958             {'type' => 'brace_command_arg',
1959              'contents' => [
1960                 @{$self->{'node'}->{'extra'}->{'node_content'}},
1961                 {'text' => "-Footnote-$self->{'footnote_index'}"}
1962              ]
1963             }
1964           ]
1965          },
1966          {'text' => ')'}],
1967          });
1968      }
1969      return $result;
1970    } elsif ($command eq 'anchor') {
1971      $result = _count_added($self, $formatter->{'container'},
1972                   add_pending_word($formatter->{'container'}));
1973      $result .= $self->_anchor($root);
1974      return $result;
1975    } elsif ($ref_commands{$command}) {
1976      if (scalar(@{$root->{'args'}})) {
1977        my @args;
1978        for my $a (@{$root->{'args'}}) {
1979          if (defined $a->{'contents'} and @{$a->{'contents'}}) {
1980            push @args, $a->{'contents'};
1981          } else {
1982            push @args, undef;
1983          }
1984        }
1985        $args[0] = [{'text' => ''}] if (!defined($args[0]));
1986
1987        # normalize node name, to get a ref with the right formatting
1988        # NOTE as a consequence, the line numbers appearing in case of errors
1989        # correspond to the node lines numbers, and not the @ref.
1990        my $node_content;
1991        if ($root->{'extra'}
1992            and $root->{'extra'}->{'label'}) {
1993          $node_content = $root->{'extra'}->{'label'}->{'extra'}->{'node_content'};
1994        } else {
1995          $node_content = $args[0];
1996        }
1997
1998        # if it a reference to a float with a label, $arg[1] is
1999        # set to '$type $number' or '$number' if there is no type.
2000        if (! defined($args[1])
2001            and $root->{'extra'}
2002            and $root->{'extra'}->{'label'}
2003            and $root->{'extra'}->{'label'}->{'cmdname'}
2004            and $root->{'extra'}->{'label'}->{'cmdname'} eq 'float') {
2005          my $float = $root->{'extra'}->{'label'};
2006
2007          my $name = $self->_float_type_number($float);
2008          $args[1] = $name->{'contents'};
2009        }
2010        if ($command eq 'inforef' and scalar(@args) == 3) {
2011          $args[3] = $args[2];
2012          $args[2] = undef;
2013        }
2014
2015        # Treat cross-reference commands in a multitable cell as if they
2016        # were surrounded by @w{ ... }, so the output will not be split across
2017        # lines, leading text from other columns appearing to be part of the
2018        # cross-reference.
2019        my $in_multitable = 0;
2020        if ($self->{'document_context'}->[-1]->{'in_multitable'}) {
2021          $in_multitable = 1;
2022          $formatter->{'w'}++;
2023          set_space_protection($formatter->{'container'}, 1, undef)
2024            if ($formatter->{'w'} == 1);
2025        }
2026        # Disallow breaks in runs of Chinese text in node names, because a
2027        # break would be normalized to a single space by the Info reader, and
2028        # the node wouldn't be found.
2029        set_space_protection($formatter->{'container'},
2030                    undef, undef, undef, undef, 1);
2031
2032        if ($command eq 'xref') {
2033          $result = _convert($self, {'contents' => [{'text' => '*Note '}]});
2034        } else {
2035          $result = _convert($self, {'contents' => [{'text' => '*note '}]});
2036        }
2037        my $name;
2038        if (defined($args[1])) {
2039          $name = $args[1];
2040        } elsif (defined($args[2])) {
2041          $name = $args[2];
2042        }
2043        my $file;
2044        if (defined($args[3])) {
2045          $file = [{'text' => '('},
2046                   {'type' => '_code',
2047                    'contents' => $args[3]},
2048                   {'text' => ')'},];
2049        } elsif (defined($args[4])) {
2050          # add a () such that the node is considered to be external,
2051          # even though the manual name is not known.
2052          $file = [{'text' => '()'}];
2053        }
2054
2055        if ($name) {
2056          my $name_text = _convert($self, {'contents' => $name});
2057          # needed, as last word is added only when : is added below
2058          my $name_text_checked = $name_text
2059             .get_pending($self->{'formatters'}->[-1]->{'container'});
2060          my $quoting_required = 0;
2061          if ($name_text_checked =~ /:/m) {
2062              if ($self->get_conf('INFO_SPECIAL_CHARS_WARNING')) {
2063                $self->line_warn(sprintf(__(
2064                   "\@%s cross-reference name should not contain `:'"),
2065                                               $command), $root->{'line_nr'});
2066              }
2067              if ($self->get_conf('INFO_SPECIAL_CHARS_QUOTE')) {
2068                $quoting_required = 1;
2069              }
2070          }
2071          my $pre_quote = $quoting_required ? "\x{7f}" : '';
2072          my $post_quote = $pre_quote;
2073          $name_text .= _convert($self, {'contents' => [
2074                {'text' => "$post_quote: "}]});
2075          $name_text =~ s/^(\s*)/$1$pre_quote/ if $pre_quote;
2076          $result .= $name_text;
2077          _count_added($self,$self->{'formatters'}[-1]{'container'},
2078                       $pre_quote)
2079            if $pre_quote;
2080
2081          if ($file) {
2082            $result .= _convert($self, {'contents' => $file});
2083          }
2084          # node name
2085          $self->{'formatters'}->[-1]->{'suppress_styles'} = 1;
2086
2087          my $maybe_file
2088            = get_pending($self->{'formatters'}->[-1]->{'container'});
2089          my $node_text = _convert($self, {'type' => '_code',
2090                                           'contents' => $node_content});
2091          delete $self->{'formatters'}->[-1]->{'suppress_styles'};
2092
2093          my $node_text_checked = $node_text
2094             .get_pending($self->{'formatters'}->[-1]->{'container'});
2095          $maybe_file =~ s/^\s*//;
2096          $maybe_file = quotemeta $maybe_file;
2097          $node_text_checked =~ s/^\s*$maybe_file//;
2098          $quoting_required = 0;
2099          if ($node_text_checked =~ /([,\t\.])/m ) {
2100              if ($self->get_conf('INFO_SPECIAL_CHARS_WARNING')) {
2101                $self->line_warn(sprintf(__(
2102                   "\@%s node name should not contain `%s'"), $command, $1),
2103                                 $root->{'line_nr'});
2104              }
2105              if ($self->get_conf('INFO_SPECIAL_CHARS_QUOTE')) {
2106                $quoting_required = 1;
2107              }
2108          }
2109          $pre_quote = $quoting_required ? "\x{7f}" : '';
2110          $post_quote = $pre_quote;
2111          if ($pre_quote) {
2112            $node_text =~ s/^(\s*)/$1$pre_quote/;
2113            _count_added($self,$self->{'formatters'}[-1]{'container'},
2114                         $pre_quote);
2115          }
2116          $result .= $node_text;
2117          _count_added($self, $self->{'formatters'}[-1]{'container'},
2118            add_next($self->{'formatters'}->[-1]->{'container'}, $post_quote))
2119                 if $post_quote;
2120        } else { # Label same as node specification
2121          if ($file) {
2122            $result .= _convert($self, {'contents' => $file});
2123          }
2124          $self->{'formatters'}->[-1]->{'suppress_styles'} = 1;
2125          my $node_text = _convert($self, {'type' => '_code',
2126                                           'contents' => $node_content});
2127          delete $self->{'formatters'}->[-1]->{'suppress_styles'};
2128
2129          my $node_text_checked = $node_text
2130             .get_pending($self->{'formatters'}->[-1]->{'container'});
2131          my $quoting_required = 0;
2132          if ($node_text_checked =~ /:/m) {
2133            if ($self->get_conf('INFO_SPECIAL_CHARS_WARNING')) {
2134              $self->line_warn(sprintf(__(
2135                 "\@%s node name should not contain `:'"), $command),
2136                               $root->{'line_nr'});
2137            }
2138            if ($self->get_conf('INFO_SPECIAL_CHARS_QUOTE')) {
2139              $quoting_required = 1;
2140            }
2141          }
2142          my $pre_quote = $quoting_required ? "\x{7f}" : '';
2143          my $post_quote = $pre_quote;
2144          $node_text .= _convert($self, {'contents' => [
2145                {'text' => "${post_quote}::"}]});
2146          _count_added($self,$self->{'formatters'}[-1]{'container'},
2147                       $pre_quote)
2148            if $pre_quote;
2149          if ($pre_quote) {
2150            # This is needed to get a pending word.  We could use
2151            # add_pending_word, but that would not include following
2152            # punctuation in the word.
2153            my $next = $self->{'current_contents'}->[-1]->[0];
2154            if ($next) {
2155              $node_text .= _convert($self, $next);
2156              shift @{$self->{'current_contents'}->[-1]};
2157            }
2158
2159            $node_text =~ s/^(\s*)/$1$pre_quote/;
2160          }
2161          $result .= $node_text;
2162        }
2163        # we could use $formatter, but in case it was changed in _convert
2164        # we play it safe.
2165        my $pending = $result
2166             .get_pending($self->{'formatters'}->[-1]->{'container'});
2167
2168        # If command is @xref, the punctuation must always follow the
2169        # command, for other commands it may be in the argument, hence the
2170        # use of $pending.
2171        # FIXME: is @xref really special here?
2172        if ($name and ($command eq 'xref'
2173            or ($pending !~ /[\.,]$/ and $pending !~ /::$/))) {
2174          my $next = $self->{'current_contents'}->[-1]->[0];
2175          if (!($next and $next->{'text'} and $next->{'text'} =~ /^[\.,]/)) {
2176            if ($command eq 'xref') {
2177              if ($next and defined($next->{'text'}) and $next->{'text'} =~ /\S/) {
2178                my $text = $next->{'text'};
2179                $text =~ s/^\s*//;
2180                my $char = substr($text, 0, 1);
2181                $self->line_warn(sprintf(__(
2182                            "`.' or `,' must follow \@xref, not %s"),
2183                                         $char), $root->{'line_nr'});
2184              } else {
2185                $self->line_warn(__("`.' or `,' must follow \@xref"),
2186                                 $root->{'line_nr'});
2187              }
2188            }
2189            my @added = ({'text' => '.'});
2190            # The added full stop does not end a sentence.  Info readers will
2191            # have a chance of guessing correctly whether the full stop was
2192            # added by whether it is followed by 2 spaces (although this
2193            # doesn't help at the end of a line).
2194            push @added, {'cmdname' => ':'};
2195            unshift @{$self->{'current_contents'}->[-1]}, @added;
2196          }
2197        }
2198
2199        if ($in_multitable) {
2200          $formatter->{'w'}--;
2201          set_space_protection($formatter->{'container'}, 0, undef)
2202            if ($formatter->{'w'} == 0);
2203        }
2204        set_space_protection($formatter->{'container'},
2205          undef,undef,undef,undef,0); # double_width_no_break
2206        return $result;
2207      }
2208      return '';
2209    } elsif ($explained_commands{$command}) {
2210      if ($root->{'args'}
2211          and defined($root->{'args'}->[0])
2212          and @{$root->{'args'}->[0]->{'contents'}}) {
2213        # in abbr spaces never end a sentence.
2214        my $argument;
2215        if ($command eq 'abbr') {
2216          $argument = {'type' => 'frenchspacing',
2217                       'contents' => $root->{'args'}->[0]->{'contents'}};
2218        } else {
2219          $argument = { 'contents' => $root->{'args'}->[0]->{'contents'}};
2220        }
2221        if (scalar (@{$root->{'args'}}) == 2
2222            and defined($root->{'args'}->[-1])
2223            and @{$root->{'args'}->[-1]->{'contents'}}) {
2224          my $prepended = $self->gdt('{abbr_or_acronym} ({explanation})',
2225                           {'abbr_or_acronym' => $argument,
2226                            'explanation' => $root->{'args'}->[-1]->{'contents'}});
2227          unshift @{$self->{'current_contents'}->[-1]}, $prepended;
2228          return '';
2229        } else {
2230          $result = _convert($self, $argument);
2231
2232          # We want to permit an end of sentence, but not force it as @. does.
2233          allow_end_sentence($formatter->{'container'});
2234          return $result;
2235        }
2236      }
2237      return '';
2238    } elsif ($inline_commands{$command}) {
2239      my $arg_index = 1;
2240      if ($command eq 'inlinefmtifelse'
2241          and (!$root->{'extra'}->{'format'}
2242               or !$self->{'expanded_formats_hash'}->{$root->{'extra'}->{'format'}})) {
2243        $arg_index = 2;
2244      }
2245      if (scalar(@{$root->{'args'}}) > $arg_index
2246         and defined($root->{'args'}->[$arg_index])
2247         and @{$root->{'args'}->[$arg_index]->{'contents'}}) {
2248        my $argument;
2249        if ($command eq 'inlineraw') {
2250          $argument->{'type'} = '_code';
2251        }
2252        $argument->{'contents'} = $root->{'args'}->[$arg_index]->{'contents'};
2253        unshift @{$self->{'current_contents'}->[-1]}, ($argument);
2254      }
2255      return '';
2256      # condition should actually be that the $command is inline
2257    } elsif ($math_commands{$command} and not exists($block_commands{$command})) {
2258      push @{$self->{'context'}}, $command;
2259      if ($root->{'args'}) {
2260        $result .= _convert($self, {'type' => 'frenchspacing',
2261             'contents' => [{'type' => '_code',
2262                            'contents' => [$root->{'args'}->[0]]}]});
2263      }
2264      my $old_context = pop @{$self->{'context'}};
2265      die if ($old_context ne $command);
2266      return $result;
2267    } elsif ($command eq 'titlefont') {
2268      push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0};
2269      $result = $self->convert_line ({'type' => 'frenchspacing',
2270               'contents' => [$root->{'args'}->[0]]});
2271      pop @{$self->{'count_context'}};
2272      $result = Texinfo::Convert::Text::heading({'level' => 0,
2273        'cmdname' => 'titlefont'}, $result, $self,
2274        $self->get_conf('NUMBER_SECTIONS'),
2275        ($self->{'format_context'}->[-1]->{'indent_level'}) * $indent_length);
2276      $result =~ s/\n$//; # final newline has its own tree element
2277      $self->{'empty_lines_count'} = 0 unless ($result eq '');
2278      _add_text_count($self, $result);
2279      _add_lines_count($self, 1);
2280      return $result;
2281
2282    } elsif ($command eq 'U') {
2283      my $arg;
2284      if ($root->{'args'}
2285          and $root->{'args'}->[0]
2286          and $root->{'args'}->[0]->{'contents'}
2287          and $root->{'args'}->[0]->{'contents'}->[0]
2288          and $root->{'args'}->[0]->{'contents'}->[0]->{'text'}) {
2289        $arg = $root->{'args'}->[0]->{'contents'}->[0]->{'text'};
2290      }
2291      if ($arg) {
2292        # Syntactic checks on the value were already done in Parser.pm,
2293        # but we have one more thing to test: since this is the one
2294        # place where we might output actual UTF-8 binary bytes, we have
2295        # to check that chr(hex($arg)) is valid.  Perl gives a warning
2296        # and will not output UTF-8 for Unicode non-characters such as
2297        # U+10FFFF.  In this case, silently fall back to plain text, on
2298        # the theory that the user wants something.
2299        my $res;
2300        if ($self->{'to_utf8'}) {
2301          my $error = 0;
2302          # The warning about non-characters is only given when the code
2303          # point is attempted to be output, not just manipulated.
2304          # http://stackoverflow.com/questions/5127725/how-could-i-catch-an-unicode-non-character-warning
2305          #
2306          # Therefore, we have to try to output it within an eval.
2307          # Since opening /dev/null or a temporary file means
2308          # more system-dependent checks, use a string as our
2309          # filehandle.
2310          eval {
2311            use warnings FATAL => qw(all);
2312            my ($fh, $string);
2313            open($fh, ">", \$string) || die "open(U string eval) failed: $!";
2314            binmode($fh, ":utf8") || die "binmode(U string eval) failed: $!";
2315            print $fh chr(hex("$arg"));
2316          };
2317          if ($@) {
2318            warn "\@U chr(hex($arg)) eval failed: $@\n" if ($self->{'DEBUG'});
2319            $error = 1;
2320          } elsif (hex($arg) > 0x10FFFF) {
2321            # The check above appears not to work in older versions of perl,
2322            # so check the argument is not greater the maximum Unicode code
2323            # point.
2324            $error = 1;
2325          }
2326          if ($error) {
2327            $res = "U+$arg";
2328          } else {
2329            $res = chr(hex($arg)); # ok to call chr
2330          }
2331        } else {
2332          $res = "U+$arg";  # not outputting UTF-8
2333        }
2334        $result .= _count_added($self, $formatter->{'container'},
2335                   add_text($formatter->{'container'}, $res));
2336      } else {
2337        $result = '';  # arg was not defined
2338      }
2339      return $result;
2340
2341    } elsif ($command eq 'value') {
2342      my $expansion = $self->gdt('@{No value for `{value}\'@}',
2343                                    {'value' => $root->{'type'}});
2344      if ($formatter->{'_top_formatter'}) {
2345        $expansion = {'type' => 'paragraph',
2346                      'contents' => [$expansion]};
2347      }
2348      $result .= _convert($self, $expansion);
2349      #  unshift @{$self->{'current_contents'}->[-1]}, $expansion;
2350      #return '';
2351      return $result;
2352    } elsif ($root->{'args'} and $root->{'args'}->[0]
2353             and $root->{'args'}->[0]->{'type'}
2354             and $root->{'args'}->[0]->{'type'} eq 'brace_command_arg') {
2355    # block commands
2356    } elsif (exists($block_commands{$command})) {
2357      # remark:
2358      # cartouche group and raggedright -> nothing on format stack
2359
2360      if ($menu_commands{$command} and $self->get_conf('FORMAT_MENU') eq 'nomenu') {
2361        return '';
2362      }
2363      if ($self->{'preformatted_context_commands'}->{$command}
2364          or $command eq 'float') {
2365        if ($self->{'formatters'}->[-1]->{'type'} eq 'paragraph'
2366            and $format_raw_commands{$command}) {
2367          $result .= _count_added($self, $formatter->{'container'},
2368                              add_pending_word($formatter->{'container'}, 1));
2369          $result .= _count_added($self, $formatter->{'container'},
2370                              end_line($formatter->{'container'}));
2371        }
2372        push @{$self->{'context'}}, $command;
2373      } elsif ($flush_commands{$command}) {
2374        push @{$self->{'context'}}, $command;
2375      } elsif ($raw_commands{$command} or $block_math_commands{$command}) {
2376        if (!$self->{'formatters'}->[-1]->{'_top_formatter'}) {
2377          # reuse the current formatter if not in top level
2378          $result .= _count_added($self, $formatter->{'container'},
2379                              add_pending_word($formatter->{'container'}, 1));
2380          $result .= _count_added($self, $formatter->{'container'},
2381                              end_line($formatter->{'container'}));
2382        } else {
2383          # if in top level, the raw block command is turned into a
2384          # simple preformatted command (alike @verbatim), to have a
2385          # formatter container being created.
2386          push @{$self->{'context'}}, $command;
2387          $self->{'format_context_commands'}->{$command} = 1;
2388          $self->{'preformatted_context_commands'}->{$command} = 1;
2389        }
2390      }
2391
2392      if ($self->{'format_context_commands'}->{$command}) {
2393        push @{$self->{'format_context'}},
2394             { 'cmdname' => $command,
2395               'paragraph_count' => 0,
2396               'indent_level' =>
2397                   $self->{'format_context'}->[-1]->{'indent_level'},
2398             };
2399        $self->{'format_context'}->[-1]->{'indent_level'}++
2400           if ($indented_commands{$command});
2401        # open a preformatted container, if the command opening the
2402        # preformatted context is not a classical preformatted
2403        # command (ie if it is menu or verbatim, and not example or
2404        # similar)
2405        if ($self->{'preformatted_context_commands'}->{$command}
2406            and ! $preformatted_commands{$command}
2407            and ! $format_raw_commands{$command}) {
2408          $preformatted = $self->new_formatter('unfilled');
2409          push @{$self->{'formatters'}}, $preformatted;
2410        }
2411      }
2412      if ($command eq 'quotation'
2413          or $command eq 'smallquotation') {
2414        if ($root->{'args'} and $root->{'args'}->[0]
2415            and $root->{'args'}->[0]->{'contents'}
2416            and @{$root->{'args'}->[0]->{'contents'}}) {
2417          my $prepended = $self->gdt('@b{{quotation_arg}:} ',
2418             {'quotation_arg' => $root->{'args'}->[0]->{'contents'}});
2419          $prepended->{'type'} = 'frenchspacing';
2420          $result .= $self->convert_line($prepended);
2421          $self->{'text_element_context'}->[-1]->{'counter'} +=
2422             Texinfo::Convert::Unicode::string_width($result);
2423          $self->{'empty_lines_count'} = 0 unless ($result eq '');
2424        }
2425      } elsif ($menu_commands{$command}) {
2426        $result .= $self->_menu($root);
2427      } elsif ($command eq 'multitable') {
2428        my $columnsize;
2429        if ($root->{'extra'}->{'columnfractions'}) {
2430          foreach my $fraction (@{$root->{'extra'}->{'columnfractions'}
2431                                       ->{'extra'}->{'misc_args'}}) {
2432            push @$columnsize, int($fraction * $self->{'text_element_context'}->[-1]->{'max'} +0.5);
2433          }
2434        } elsif ($root->{'extra'}->{'prototypes'}) {
2435          foreach my $prototype (@{$root->{'extra'}->{'prototypes'}}) {
2436            push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0};
2437            my ($formatted_prototype) = $self->convert_line($prototype,
2438                                                        {'indent_length' => 0});
2439            pop @{$self->{'count_context'}};
2440            push @$columnsize,
2441                 2+Texinfo::Convert::Unicode::string_width($formatted_prototype);
2442          }
2443        }
2444        $self->{'format_context'}->[-1]->{'columns_size'} = $columnsize;
2445        $self->{'format_context'}->[-1]->{'row_empty_lines_count'}
2446          = $self->{'empty_lines_count'};
2447        $self->{'document_context'}->[-1]->{'in_multitable'}++;
2448      } elsif ($command eq 'float') {
2449        $result .= _add_newline_if_needed($self);
2450        if ($root->{'extra'} and $root->{'extra'}->{'node_content'}) {
2451          $result .= $self->_anchor($root);
2452        }
2453      }
2454    } elsif ($command eq 'node') {
2455      $self->{'node'} = $root;
2456      $result .= $self->_node($root);
2457      $self->{'format_context'}->[-1]->{'paragraph_count'} = 0;
2458    } elsif ($sectioning_commands{$command}) {
2459      # use settitle for empty @top
2460      # ignore @part
2461      my $contents;
2462      if ($root->{'args'}->[0]->{'contents'}
2463          and @{$root->{'args'}->[0]->{'contents'}}
2464          and $command ne 'part') {
2465        $contents = $root->{'args'}->[0]->{'contents'};
2466      } elsif ($command eq 'top'
2467          and $self->{'extra'}->{'settitle'}
2468          and $self->{'extra'}->{'settitle'}->{'args'}
2469          and @{$self->{'extra'}->{'settitle'}->{'args'}}
2470          and $self->{'extra'}->{'settitle'}->{'args'}->[0]->{'contents'}
2471          and @{$self->{'extra'}->{'settitle'}->{'args'}->[0]->{'contents'}}) {
2472        $contents = $self->{'extra'}->{'settitle'}->{'args'}->[0]->{'contents'};
2473      }
2474
2475      if ($contents) {
2476        push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0};
2477        my $heading = $self->convert_line({'type' => 'frenchspacing',
2478                         'contents' => $contents});
2479        pop @{$self->{'count_context'}};
2480        # @* leads to an end of line, underlying appears on the line below
2481        # over one line
2482        my $heading_underlined =
2483             Texinfo::Convert::Text::heading($root, $heading, $self,
2484                                             $self->get_conf('NUMBER_SECTIONS'),
2485                           ($self->{'format_context'}->[-1]->{'indent_level'})
2486                                           * $indent_length);
2487        $result .= _add_newline_if_needed($self);
2488        $self->{'empty_lines_count'} = 0 unless ($heading_underlined eq '');
2489        _add_text_count($self, $heading_underlined);
2490        $result .= $heading_underlined;
2491        if ($heading_underlined ne '') {
2492          _add_lines_count($self, 2);
2493          $result .= _add_newline_if_needed($self);
2494        }
2495      }
2496      $self->{'format_context'}->[-1]->{'paragraph_count'} = 0;
2497    } elsif (($command eq 'item' or $command eq 'itemx')
2498            and $root->{'args'} and $root->{'args'}->[0]
2499            and $root->{'args'}->[0]->{'type'}
2500            and $root->{'args'}->[0]->{'type'} eq 'line_arg') {
2501      if ($root->{'args'} and @{$root->{'args'}}
2502          and $root->{'args'}->[0]->{'contents'}) {
2503
2504        my $converted_tree = $self->_table_item_content_tree($root,
2505                                         $root->{'args'}->[0]->{'contents'});
2506
2507        $converted_tree->{'type'} = 'frenchspacing';
2508        $result = $self->convert_line($converted_tree,
2509                    {'indent_level'
2510                      => $self->{'format_context'}->[-1]->{'indent_level'} -1});
2511        if ($result ne '') {
2512          $result = $self->ensure_end_of_line($result);
2513          $self->{'empty_lines_count'} = 0;
2514        }
2515      }
2516    } elsif ($command eq 'item' and $root->{'parent'}->{'cmdname'}
2517             and $item_container_commands{$root->{'parent'}->{'cmdname'}}) {
2518      $self->{'format_context'}->[-1]->{'paragraph_count'} = 0;
2519      my $line = $self->new_formatter('line',
2520          {'indent_length' =>
2521              ($self->{'format_context'}->[-1]->{'indent_level'} -1)
2522                * $indent_length
2523                 + $item_indent_format_length{$root->{'parent'}->{'cmdname'}}});
2524      push @{$self->{'formatters'}}, $line;
2525      if ($root->{'parent'}->{'cmdname'} eq 'enumerate') {
2526        $result = _count_added($self, $line->{'container'},
2527            add_next($line->{'container'},
2528               Texinfo::Common::enumerate_item_representation(
2529                 $root->{'parent'}->{'extra'}->{'enumerate_specification'},
2530                 $root->{'extra'}->{'item_number'}) . '. '));
2531      } elsif ($root->{'parent'}->{'args'}
2532          and $root->{'parent'}->{'args'}->[0]) {
2533        # this is the text prepended to items.
2534
2535        $result = _convert($self, $root->{'parent'}->{'args'}->[0]);
2536        $result .= _convert($self, { 'text' => ' ' });
2537      }
2538      $result .= _count_added($self, $line->{'container'},
2539                      Texinfo::Convert::Paragraph::end($line->{'container'}));
2540      pop @{$self->{'formatters'}};
2541      $self->{'text_element_context'}->[-1]->{'counter'} +=
2542         Texinfo::Convert::Unicode::string_width($result);
2543      $self->{'empty_lines_count'} = 0 unless ($result eq '');
2544    # open a multitable cell
2545    } elsif ($command eq 'headitem' or $command eq 'item'
2546             or $command eq 'tab') {
2547      my $cell_width = $self->{'format_context'}->[-1]->{'columns_size'}->[$root->{'extra'}->{'cell_number'}-1];
2548      $self->{'format_context'}->[-1]->{'item_command'} = $command
2549        if ($command ne 'tab');
2550      die if (!defined($cell_width));
2551      $self->{'empty_lines_count'}
2552         = $self->{'format_context'}->[-1]->{'row_empty_lines_count'};
2553
2554      push @{$self->{'format_context'}},
2555           { 'cmdname' => $command,
2556             'paragraph_count' => 0,
2557             'indent_level' => 0 };
2558      push @{$self->{'text_element_context'}}, {'max' => $cell_width - 2 };
2559      push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0,
2560                                                   'locations' => []};
2561      $cell = 1;
2562    } elsif ($command eq 'center') {
2563      #my ($counts, $new_locations);
2564      push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0,
2565                                                   'locations' => []};
2566      $result = $self->convert_line (
2567                       {'type' => 'frenchspacing',
2568                        'contents' => $root->{'args'}->[0]->{'contents'}},
2569                       {'indent_length' => 0});
2570      if ($result ne '') {
2571        $result = $self->ensure_end_of_line($result);
2572
2573        $result = $self->_align_environment ($result,
2574             $self->{'text_element_context'}->[-1]->{'max'}, 'center');
2575        $self->{'empty_lines_count'} = 0;
2576      } else {
2577        # it has to be done here, as it is done in _align_environment above
2578        pop @{$self->{'count_context'}};
2579      }
2580      $self->{'format_context'}->[-1]->{'paragraph_count'}++;
2581      return $result;
2582    } elsif ($command eq 'exdent') {
2583      if ($self->{'preformatted_context_commands'}->{$self->{'context'}->[-1]}) {
2584        $result = $self->convert_unfilled({'contents' => $root->{'args'}->[0]->{'contents'}},
2585         {'indent_level'
2586          => $self->{'format_context'}->[-1]->{'indent_level'} -1});
2587      } else {
2588        $result = $self->convert_line({'contents' => $root->{'args'}->[0]->{'contents'}},
2589         {'indent_level'
2590          => $self->{'format_context'}->[-1]->{'indent_level'} -1});
2591      }
2592      if ($result ne '') {
2593        $result = $self->ensure_end_of_line($result);
2594        $self->{'empty_lines_count'} = 0;
2595      }
2596      return $result;
2597    } elsif ($command eq 'verbatiminclude') {
2598      my $expansion = $self->Texinfo::Common::expand_verbatiminclude($root);
2599      unshift @{$self->{'current_contents'}->[-1]}, $expansion
2600        if ($expansion);
2601      return '';
2602    } elsif ($command eq 'insertcopying') {
2603      if ($self->{'extra'} and $self->{'extra'}->{'copying'}) {
2604        unshift @{$self->{'current_contents'}->[-1]},
2605           {'contents' => $self->{'extra'}->{'copying'}->{'contents'}};
2606      }
2607      return '';
2608    } elsif ($command eq 'printindex') {
2609      $result = $self->_printindex($root);
2610      return $result;
2611    } elsif ($command eq 'listoffloats') {
2612      my $lines_count = 0;
2613      if ($root->{'extra'} and $root->{'extra'}->{'type'}
2614          and defined($root->{'extra'}->{'type'}->{'normalized'})
2615          and $self->{'floats'}
2616          and $self->{'floats'}->{$root->{'extra'}->{'type'}->{'normalized'}}
2617          and @{$self->{'floats'}->{$root->{'extra'}->{'type'}->{'normalized'}}}) {
2618        push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0};
2619        if (!$self->{'empty_lines_count'}) {
2620          $result .= "\n";
2621          $lines_count++;
2622        }
2623        $result .= "* Menu:\n\n";
2624        $lines_count += 2;
2625        foreach my $float (@{$self->{'floats'}->{$root->{'extra'}->{'type'}->{'normalized'}}}) {
2626          next if !$float->{'args'} or !$float->{'args'}->[1]
2627                   or !@{$float->{'args'}->[1]->{'contents'}};
2628          my $float_label_text = $self->convert_line({'type' => '_code',
2629             'contents' => $float->{'args'}->[1]->{'contents'}});
2630          my $float_entry = $self->_float_type_number($float);
2631          my $float_entry_text = ':';
2632          if (defined($float_entry)) {
2633            $float_entry->{'type'} = 'frenchspacing';
2634            $float_entry_text = $self->convert_line($float_entry);
2635          }
2636          # no translation here, this is required Info format.
2637          my $float_line = "* $float_entry_text: $float_label_text.";
2638          my $line_width
2639             = Texinfo::Convert::Unicode::string_width($float_line);
2640          if ($line_width > $listoffloat_entry_length) {
2641            $float_line .= "\n" . ' ' x $listoffloat_entry_length;
2642            $lines_count++;
2643          } else {
2644            $float_line .= ' ' x ($listoffloat_entry_length - $line_width);
2645          }
2646          $line_width = $listoffloat_entry_length;
2647          my $caption;
2648          if ($float->{'extra'}->{'shortcaption'}) {
2649            $caption = $float->{'extra'}->{'shortcaption'};
2650          } elsif ($float->{'extra'}->{'caption'}) {
2651            $caption = $float->{'extra'}->{'caption'};
2652          }
2653          if ($caption) {
2654            $self->{'multiple_pass'} = 1;
2655            push @{$self->{'context'}}, 'listoffloats';
2656            my $tree = {'contents' => $caption->{'args'}->[0]->{'contents'}};
2657            # the following does nothing since there are paragraphs within
2658            # the shortcaption.
2659            #if ($caption->{'cmdname'} eq 'shortcaption') {
2660            #  $tree->{'type'} = 'frenchspacing';
2661            #}
2662            my $caption_text = _convert($self, $tree);
2663            my $old_context = pop @{$self->{'context'}};
2664            delete $self->{'multiple_pass'};
2665            die if ($old_context ne 'listoffloats');
2666            while ($caption_text =~ s/^\s*(\p{Unicode::EastAsianWidth::InFullwidth}\s*|\S+\s*)//) {
2667              my $new_word = $1;
2668              $new_word =~ s/\n//g;
2669              if ((Texinfo::Convert::Unicode::string_width($new_word) +
2670                   $line_width) >
2671                       ($self->{'text_element_context'}->[-1]->{'max'} - 3)) {
2672                $float_line .= $listoffloat_append;
2673                last;
2674              } else {
2675                $float_line .= $new_word;
2676                $line_width +=
2677                  Texinfo::Convert::Unicode::string_width($new_word);
2678              }
2679            }
2680          }
2681          $result .= $float_line. "\n";
2682          $lines_count++;
2683        }
2684        $result .= "\n";
2685        $lines_count++;
2686        $self->{'empty_lines_count'} = 1;
2687        pop @{$self->{'count_context'}};
2688      }
2689      $self->{'format_context'}->[-1]->{'paragraph_count'}++;
2690      _add_text_count($self, $result);
2691      _add_lines_count($self, $lines_count);
2692      return $result;
2693    } elsif ($command eq 'sp') {
2694      if ($root->{'extra'}->{'misc_args'}->[0]) {
2695        $result = _count_added($self, $formatter->{'container'},
2696                              add_pending_word($formatter->{'container'}));
2697        # this useless copy avoids perl changing the type to integer!
2698        my $sp_nr = $root->{'extra'}->{'misc_args'}->[0];
2699        for (my $i = 0; $i < $sp_nr; $i++) {
2700          $result .= _count_added($self, $formatter->{'container'},
2701                end_line($formatter->{'container'}));
2702        }
2703
2704        $self->{'empty_lines_count'} += $sp_nr;
2705        delete $self->{'text_element_context'}->[-1]->{'counter'};
2706      }
2707      return $result;
2708    } elsif ($command eq 'contents') {
2709      if ($self->{'structuring'}
2710            and $self->{'structuring'}->{'sectioning_root'}) {
2711        my $lines_count;
2712        ($result, $lines_count)
2713            = $self->_contents($self->{'structuring'}->{'sectioning_root'},
2714                              'contents');
2715        _add_lines_count($self, $lines_count);
2716        _add_text_count($self, $result);
2717      }
2718      return $result;
2719    } elsif ($command eq 'shortcontents'
2720               or $command eq 'summarycontents') {
2721      if ($self->{'structuring'}
2722            and $self->{'structuring'}->{'sectioning_root'}) {
2723        my $lines_count;
2724        ($result, $lines_count)
2725              = $self->_contents($self->{'structuring'}->{'sectioning_root'},
2726                              'shortcontents');
2727        _add_lines_count($self, $lines_count);
2728        _add_text_count($self, $result);
2729      }
2730      return $result;
2731    # all the @-commands that have an information for the formatting, like
2732    # @paragraphindent, @frenchspacing...
2733    } elsif ($informative_commands{$command}) {
2734      $self->_informative_command($root);
2735      return '';
2736    } else {
2737      $unknown_command = 1;
2738    }
2739    if ($unknown_command
2740        and !($root->{'extra'}
2741                and ($root->{'extra'}->{'index_entry'}
2742                     or $root->{'extra'}->{'seeentry'}
2743                     or $root->{'extra'}->{'seealso'}))
2744        # commands like def*x are not processed above, since only the def_line
2745        # associated is processed. If they have no name and no category they
2746        # are not considered as index entries either so they have a specific
2747        # condition
2748        and !($def_commands{$command}
2749              and $command =~ /x$/)) {
2750      warn "Unhandled $command\n";
2751      $result .= "!!!!!!!!! Unhandled $command !!!!!!!!!\n";
2752    }
2753  }
2754
2755  # open 'type' constructs.
2756  my $paragraph;
2757  if ($root->{'type'}) {
2758    if ($root->{'type'} eq 'paragraph') {
2759      $self->{'empty_lines_count'} = 0;
2760      my $conf;
2761      # indent. Not first paragraph.
2762      if ($self->{'format_context'}->[-1]->{'cmdname'} eq '_top_format'
2763          and $self->get_conf('paragraphindent') ne 'asis'
2764          and $self->get_conf('paragraphindent')
2765          and (($root->{'extra'} and $root->{'extra'}->{'indent'})
2766             or (!($root->{'extra'} and $root->{'extra'}->{'noindent'})
2767                and ($self->{'format_context'}->[-1]->{'paragraph_count'}
2768                  or $self->get_conf('firstparagraphindent') eq 'insert')
2769               and !$self->{'text_element_context'}->[-1]->{'counter'}))) {
2770        $conf->{'first_indent_length'} = $self->get_conf('paragraphindent');
2771        $conf->{'first_indent_length'} = 0
2772          if ($conf->{'first_indent_length'} eq 'none');
2773      }
2774      $paragraph = $self->new_formatter('paragraph', $conf);
2775      push @{$self->{'formatters'}}, $paragraph;
2776      $self->{'format_context'}->[-1]->{'paragraph_count'}++;
2777      if ($self->{'context'}->[-1] eq 'flushright') {
2778        push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0,
2779                                                   'locations' => []};
2780      }
2781    } elsif ($root->{'type'} eq 'preformatted'
2782             or $root->{'type'} eq 'rawpreformatted') {
2783      # if in a description reuse the main menu unfilled, to keep things
2784      # simpler and avoid having to do a separate count.
2785      if ($root->{'type'} eq 'rawpreformatted'
2786          or !$root->{'parent'}->{'type'}
2787          or $root->{'parent'}->{'type'} ne 'menu_entry_description') {
2788        $preformatted = $self->new_formatter('unfilled');
2789        push @{$self->{'formatters'}}, $preformatted;
2790        if ($self->{'context'}->[-1] eq 'flushright') {
2791          push @{$self->{'count_context'}}, {'lines' => 0, 'bytes' => 0,
2792                                                     'locations' => []};
2793        }
2794      }
2795    } elsif ($root->{'type'} eq 'def_line') {
2796      if ($root->{'extra'} and $root->{'extra'}->{'def_parsed_hash'}
2797             and %{$root->{'extra'}->{'def_parsed_hash'}}) {
2798        my $arguments = Texinfo::Common::definition_arguments_content($root);
2799        my $tree;
2800        my $command;
2801        if ($Texinfo::Common::def_aliases{$root->{'extra'}->{'def_command'}}) {
2802          $command = $Texinfo::Common::def_aliases{$root->{'extra'}->{'def_command'}};
2803        } else {
2804          $command = $root->{'extra'}->{'def_command'};
2805        }
2806        my $name;
2807        if ($root->{'extra'}->{'def_parsed_hash'}->{'name'}) {
2808          $name = $root->{'extra'}->{'def_parsed_hash'}->{'name'};
2809        } else {
2810          $name = '';
2811        }
2812
2813        if ($command eq 'deffn'
2814            or $command eq 'defvr'
2815            or $command eq 'deftp'
2816            or (($command eq 'deftypefn'
2817                 or $command eq 'deftypevr')
2818                and !$root->{'extra'}->{'def_parsed_hash'}->{'type'})) {
2819          if ($arguments) {
2820            $tree = $self->gdt("\@tie{}-- {category}: {name} {arguments}", {
2821                    'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
2822                    'name' => $name,
2823                    'arguments' => $arguments});
2824          } else {
2825            $tree = $self->gdt("\@tie{}-- {category}: {name}", {
2826                    'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
2827                    'name' => $name});
2828          }
2829        } elsif ($command eq 'deftypefn'
2830                 or $command eq 'deftypevr') {
2831          if ($arguments) {
2832            my $strings = {
2833                    'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
2834                    'name' => $name,
2835                    'type' => $root->{'extra'}->{'def_parsed_hash'}->{'type'},
2836                    'arguments' => $arguments};
2837            if ($self->get_conf('deftypefnnewline') eq 'on') {
2838              $tree = $self->gdt("\@tie{}-- {category}:\@*{type}\@*{name} {arguments}",
2839                                 $strings);
2840            } else {
2841              $tree = $self->gdt("\@tie{}-- {category}: {type} {name} {arguments}",
2842                                 $strings);
2843            }
2844          } else {
2845            my $strings = {
2846                    'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
2847                    'type' => $root->{'extra'}->{'def_parsed_hash'}->{'type'},
2848                    'name' => $name};
2849            if ($self->get_conf('deftypefnnewline') eq 'on') {
2850              $tree = $self->gdt("\@tie{}-- {category}:\@*{type}\@*{name}",
2851                                 $strings);
2852            } else {
2853              $tree = $self->gdt("\@tie{}-- {category}: {type} {name}",
2854                                 $strings);
2855            }
2856          }
2857        } elsif ($command eq 'defcv'
2858                 or ($command eq 'deftypecv'
2859                     and !$root->{'extra'}->{'def_parsed_hash'}->{'type'})) {
2860          if ($arguments) {
2861            $tree = $self->gdt("\@tie{}-- {category} of {class}: {name} {arguments}", {
2862                    'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
2863                    'name' => $name,
2864                    'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
2865                    'arguments' => $arguments});
2866          } else {
2867            $tree = $self->gdt("\@tie{}-- {category} of {class}: {name}", {
2868                    'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
2869                    'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
2870                    'name' => $name});
2871          }
2872        } elsif ($command eq 'defop'
2873                 or ($command eq 'deftypeop'
2874                     and !$root->{'extra'}->{'def_parsed_hash'}->{'type'})) {
2875          if ($arguments) {
2876            $tree = $self->gdt("\@tie{}-- {category} on {class}: {name} {arguments}", {
2877                    'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
2878                    'name' => $name,
2879                    'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
2880                    'arguments' => $arguments});
2881          } else {
2882            $tree = $self->gdt("\@tie{}-- {category} on {class}: {name}", {
2883                    'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
2884                    'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
2885                    'name' => $name});
2886          }
2887        } elsif ($command eq 'deftypeop') {
2888          if ($arguments) {
2889            my $strings = {
2890                    'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
2891                    'name' => $name,
2892                    'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
2893                    'type' => $root->{'extra'}->{'def_parsed_hash'}->{'type'},
2894                    'arguments' => $arguments};
2895            if ($self->get_conf('deftypefnnewline') eq 'on') {
2896              $tree
2897                = $self->gdt("\@tie{}-- {category} on {class}:\@*{type}\@*{name} {arguments}",
2898                             $strings);
2899            } else {
2900              $tree
2901                = $self->gdt("\@tie{}-- {category} on {class}: {type} {name} {arguments}",
2902                             $strings);
2903            }
2904          } else {
2905            my $strings = {
2906                    'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
2907                    'type' => $root->{'extra'}->{'def_parsed_hash'}->{'type'},
2908                    'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
2909                    'name' => $name};
2910            if ($self->get_conf('deftypefnnewline') eq 'on') {
2911              $tree
2912                = $self->gdt("\@tie{}-- {category} on {class}:\@*{type}\@*{name}",
2913                             $strings);
2914            } else {
2915              $tree
2916                = $self->gdt("\@tie{}-- {category} on {class}: {type} {name}",
2917                             $strings);
2918            }
2919          }
2920        } elsif ($command eq 'deftypecv') {
2921          if ($arguments) {
2922            my $strings = {
2923                    'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
2924                    'name' => $name,
2925                    'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
2926                    'type' => $root->{'extra'}->{'def_parsed_hash'}->{'type'},
2927                    'arguments' => $arguments};
2928            if ($self->get_conf('deftypefnnewline') eq 'on') {
2929              $tree
2930                = $self->gdt("\@tie{}-- {category} of {class}:\@*{type}\@*{name} {arguments}",
2931                             $strings);
2932            } else {
2933              $tree
2934                = $self->gdt("\@tie{}-- {category} of {class}: {type} {name} {arguments}",
2935                             $strings);
2936            }
2937          } else {
2938            my $strings = {
2939                    'category' => $root->{'extra'}->{'def_parsed_hash'}->{'category'},
2940                    'type' => $root->{'extra'}->{'def_parsed_hash'}->{'type'},
2941                    'class' => $root->{'extra'}->{'def_parsed_hash'}->{'class'},
2942                    'name' => $name};
2943            if ($self->get_conf('deftypefnnewline') eq 'on') {
2944              $tree
2945                = $self->gdt("\@tie{}-- {category} of {class}:\@*{type}\@*{name}",
2946                             $strings);
2947            } else {
2948              $tree
2949                = $self->gdt("\@tie{}-- {category} of {class}: {type} {name}",
2950                             $strings);
2951            }
2952          }
2953        }
2954
2955        my $def_paragraph = $self->new_formatter('paragraph',
2956         { 'indent_length' => ($self->{'format_context'}->[-1]->{'indent_level'} -1) *$indent_length,
2957           'indent_length_next' => (1+$self->{'format_context'}->[-1]->{'indent_level'})*$indent_length,
2958           'suppress_styles' => 1
2959         });
2960        push @{$self->{'formatters'}}, $def_paragraph;
2961
2962        $result .= _convert($self, {'type' => '_code', 'contents' => [$tree]});
2963        $result .= _count_added($self, $def_paragraph->{'container'},
2964              Texinfo::Convert::Paragraph::end($def_paragraph->{'container'}));
2965
2966        pop @{$self->{'formatters'}};
2967        delete $self->{'text_element_context'}->[-1]->{'counter'};
2968        $self->{'empty_lines_count'} = 0;
2969      }
2970    } elsif ($root->{'type'} eq 'menu_entry') {
2971      my $entry_name_seen = 0;
2972      foreach my $arg (@{$root->{'args'}}) {
2973        my ($pre_quote, $post_quote);
2974        if ($arg->{'type'} eq 'menu_entry_node') {
2975          $self->{'formatters'}->[-1]->{'suppress_styles'} = 1;
2976
2977          # Flush a leading space
2978          $result .= _count_added($self, $formatter->{'container'},
2979                           add_pending_word($formatter->{'container'}, 1));
2980
2981          my $node_text = _convert($self, {'type' => '_code',
2982                                      'contents' => $arg->{'contents'}});
2983
2984          $node_text .= _count_added($self, $formatter->{'container'},
2985                           add_pending_word($formatter->{'container'}, 1));
2986          delete $self->{'formatters'}->[-1]->{'suppress_styles'};
2987          $pre_quote = $post_quote = '';
2988          if ($entry_name_seen) {
2989            if ($node_text =~ /([,\t]|\.\s)/) {
2990              if ($self->get_conf('INFO_SPECIAL_CHARS_WARNING')) {
2991                $self->line_warn(sprintf(__(
2992                   "menu entry node name should not contain `%s'"), $1),
2993                               $root->{'line_nr'});
2994              }
2995            }
2996            if ($self->get_conf('INFO_SPECIAL_CHARS_QUOTE')) {
2997              $pre_quote = $post_quote = "\x{7f}";
2998            }
2999          } else {
3000            if ($node_text =~ /:/) {
3001              if ($self->get_conf('INFO_SPECIAL_CHARS_WARNING')) {
3002                $self->line_warn(__(
3003                 "menu entry node name should not contain `:'"),
3004                               $root->{'line_nr'});
3005              }
3006              if ($self->get_conf('INFO_SPECIAL_CHARS_QUOTE')) {
3007                $pre_quote = $post_quote = "\x{7f}";
3008              }
3009            }
3010          }
3011          $result .= $pre_quote . $node_text . $post_quote;
3012        } elsif ($arg->{'type'} eq 'menu_entry_name') {
3013          my $entry_name = _convert($self, $arg);
3014          my $formatter = $self->{'formatters'}->[-1];
3015          $entry_name .= _count_added($self,
3016                           $formatter->{'container'},
3017                           add_pending_word($formatter->{'container'}, 1));
3018          $entry_name_seen = 1;
3019          $pre_quote = $post_quote = '';
3020          if ($entry_name =~ /:/) {
3021            if ($self->get_conf('INFO_SPECIAL_CHARS_WARNING')) {
3022              $self->line_warn(__(
3023                 "menu entry name should not contain `:'"),
3024                               $root->{'line_nr'});
3025            }
3026            if ($self->get_conf('INFO_SPECIAL_CHARS_QUOTE')) {
3027              $pre_quote = $post_quote = "\x{7f}";
3028            }
3029          }
3030          $result .= $pre_quote . $entry_name . $post_quote;
3031        } else {
3032          $result .= _convert($self, $arg);
3033        }
3034      }
3035
3036      # If we are nested inside an @example, a 'menu_entry_description' may not
3037      # have been processed yet, and we need to output any pending spaces
3038      # before 'end_line' throws them away.  The argument to 'add_pending_word'
3039      # does this.
3040      if ($root->{'parent'}->{'type'}
3041              and $root->{'parent'}->{'type'} eq 'preformatted') {
3042        $result .= _count_added($self,
3043                         $formatter->{'container'},
3044                         add_pending_word($formatter->{'container'}, 1));
3045      } else {
3046        $result .= _count_added($self,
3047                         $formatter->{'container'},
3048                         add_pending_word($formatter->{'container'}));
3049        end_line($formatter->{'container'});
3050        $result = $self->ensure_end_of_line($result) ;
3051      }
3052
3053    } elsif ($root->{'type'} eq 'frenchspacing') {
3054      push @{$formatter->{'frenchspacing_stack'}}, 'on';
3055      set_space_protection($formatter->{'container'}, undef, undef, undef, 1);
3056    } elsif ($root->{'type'} eq '_code') {
3057      if (!$formatter->{'font_type_stack'}->[-1]->{'monospace'}) {
3058        push @{$formatter->{'font_type_stack'}}, {'monospace' => 1};
3059      } else {
3060        $formatter->{'font_type_stack'}->[-1]->{'monospace'}++;
3061      }
3062      push @{$formatter->{'frenchspacing_stack'}}, 'on';
3063      set_space_protection($formatter->{'container'}, undef, undef, undef, 1);
3064    } elsif ($root->{'type'} eq 'bracketed') {
3065      $result .= _count_added($self, $formatter->{'container'},
3066                   add_text($formatter->{'container'}, '{'));
3067    }
3068  }
3069
3070  # The processing of contents is done here.
3071  if ($root->{'contents'}) {
3072    my @contents = @{$root->{'contents'}};
3073    push @{$self->{'current_contents'}}, \@contents;
3074    push @{$self->{'current_roots'}}, $root;
3075    while (@contents) {
3076      my $content = shift @contents;
3077      my $text = _convert($self, $content);
3078      $self->{'empty_lines_count'} = 0
3079        if ($preformatted and $text =~ /\S/);
3080      $result .= $text;
3081    }
3082    pop @{$self->{'current_contents'}};
3083    pop @{$self->{'current_roots'}};
3084  }
3085
3086  # now closing. First, close types.
3087  if ($root->{'type'}) {
3088    if ($root->{'type'} eq 'frenchspacing') {
3089      pop @{$formatter->{'frenchspacing_stack'}};
3090      my $frenchspacing = 0;
3091      $frenchspacing = 1 if ($formatter->{'frenchspacing_stack'}->[-1] eq 'on');
3092      set_space_protection($formatter->{'container'}, undef,
3093                           undef, undef, $frenchspacing);
3094    } elsif ($root->{'type'} eq '_code') {
3095      $formatter->{'font_type_stack'}->[-1]->{'monospace'}--;
3096      pop @{$formatter->{'font_type_stack'}}
3097        if !$formatter->{'font_type_stack'}->[-1]->{'monospace'};
3098      pop @{$formatter->{'frenchspacing_stack'}};
3099      my $frenchspacing = 0;
3100      $frenchspacing = 1 if ($formatter->{'frenchspacing_stack'}->[-1] eq 'on');
3101      set_space_protection($formatter->{'container'}, undef,
3102                           undef, undef, $frenchspacing);
3103    } elsif ($root->{'type'} eq 'bracketed') {
3104      $result .= _count_added($self, $formatter->{'container'},
3105                                     add_text($formatter->{'container'}, '}'));
3106    } elsif ($root->{'type'} eq 'row') {
3107      my @cell_beginnings;
3108      my @cell_lines;
3109      my $cell_beginning = 0;
3110      my $cell_idx = 0;
3111      my $max_lines = 0;
3112      my $indent_len = $indent_length
3113             * $self->{'format_context'}->[-1]->{'indent_level'};
3114      foreach my $cell (@{$self->{'format_context'}->[-1]->{'row'}}) {
3115        $cell_beginnings[$cell_idx] = $cell_beginning;
3116        my $cell_width = $self->{'format_context'}->[-1]->{'columns_size'}->[$cell_idx];
3117        $cell_beginning += $cell_width +1;
3118        $cell_lines[$cell_idx] = [ split /^/, $cell ];
3119        $max_lines = scalar(@{$cell_lines[$cell_idx]})
3120          if (scalar(@{$cell_lines[$cell_idx]}) > $max_lines);
3121        $cell_idx++;
3122      }
3123
3124      $cell_idx = 0;
3125      my $cell_updated_locations;
3126      my @row_locations;
3127      foreach my $cell_locations (@{$self->{'format_context'}->[-1]->{'row_counts'}}) {
3128        foreach my $location (@{$cell_locations->{'locations'}}) {
3129          next unless (defined($location->{'bytes'}) and defined($location->{'lines'}));
3130          push @{$cell_updated_locations->[$cell_idx]->{$location->{'lines'}}},
3131                 $location;
3132          $max_lines = $location->{'lines'}+1
3133                            if ($location->{'lines'}+1 > $max_lines);
3134        }
3135        push @row_locations, @{$cell_locations->{'locations'}};
3136        $cell_idx++;
3137      }
3138
3139      # this is used to keep track of the last cell with content.
3140      my $max_cell = scalar(@{$self->{'format_context'}->[-1]->{'row'}});
3141      my $bytes_count = 0;
3142      my $line;
3143      for (my $line_idx = 0; $line_idx < $max_lines; $line_idx++) {
3144        my $line_width = $indent_len;
3145        $line = '';
3146        # determine the last cell in the line, to fill spaces in
3147        # cells preceding that cell on the line
3148        my $last_cell = 0;
3149        for (my $cell_idx = 0; $cell_idx < $max_cell; $cell_idx++) {
3150          $last_cell = $cell_idx+1 if (defined($cell_lines[$cell_idx]->[$line_idx])
3151                                       or defined($cell_updated_locations->[$cell_idx]->{$line_idx}));
3152        }
3153
3154        for (my $cell_idx = 0; $cell_idx < $last_cell; $cell_idx++) {
3155          my $cell_text = $cell_lines[$cell_idx]->[$line_idx];
3156          if (defined($cell_text)) {
3157            chomp($cell_text);
3158            if ($line eq '' and $cell_text ne '') {
3159              $line = ' ' x $indent_len;
3160              $bytes_count += count_bytes($self, $line);
3161            }
3162            $line .= $cell_text;
3163            $bytes_count += count_bytes($self, $cell_text);
3164            $line_width += Texinfo::Convert::Unicode::string_width($cell_text);
3165          }
3166          if (defined($cell_updated_locations->[$cell_idx]->{$line_idx})) {
3167            foreach my $location (@{$cell_updated_locations->[$cell_idx]->{$line_idx}}) {
3168              $location->{'bytes'} = $bytes_count;
3169            }
3170          }
3171          if ($cell_idx+1 < $last_cell) {
3172            if ($line_width < $indent_len + $cell_beginnings[$cell_idx+1]) {
3173              if ($line eq '') {
3174                $line = ' ' x $indent_len;
3175                $bytes_count += count_bytes($self, $line);
3176              }
3177              my $spaces = ' ' x ($indent_len + $cell_beginnings[$cell_idx+1] - $line_width);
3178              $line_width += Texinfo::Convert::Unicode::string_width($spaces);
3179              $line .= $spaces;
3180              $bytes_count += count_bytes($self, $spaces);
3181            }
3182          }
3183        }
3184        $line .= "\n";
3185        $bytes_count += count_bytes($self, "\n");
3186        $result .= $line;
3187      }
3188      if ($self->{'format_context'}->[-1]->{'item_command'} eq 'headitem') {
3189        # at this point cell_beginning is at the beginning of
3190        # the cell following the end of the table -> full width
3191        my $line = ' ' x $indent_len . '-' x $cell_beginning . "\n";
3192        $bytes_count += count_bytes($self, $line);
3193        $result .= $line;
3194        $self->{'empty_lines_count'} = 0;
3195        $max_lines++;
3196      # there may be empty lines, in that case $line is undef, $max_lines == 0
3197      } elsif ($max_lines) {
3198        if ($line eq "\n") {
3199          $self->{'empty_lines_count'} = 1;
3200        } else {
3201          $self->{'empty_lines_count'} = 0;
3202        }
3203      }
3204      $self->_update_locations_counts(\@row_locations);
3205      push @{$self->{'count_context'}->[-1]->{'locations'}}, @row_locations;
3206      $self->{'count_context'}->[-1]->{'bytes'} += $bytes_count;
3207      $self->{'count_context'}->[-1]->{'lines'} += $max_lines;
3208      $self->{'format_context'}->[-1]->{'row'} = [];
3209      $self->{'format_context'}->[-1]->{'row_counts'} = [];
3210      $self->{'format_context'}->[-1]->{'row_empty_lines_count'}
3211        = $self->{'empty_lines_count'};
3212    } elsif ($root->{'type'} eq 'text_root') {
3213      $self->{'text_before_first_node'} = $result;
3214    }
3215  }
3216  # close paragraphs and preformatted
3217  if ($paragraph) {
3218    $result .= _count_added($self, $paragraph->{'container'},
3219               Texinfo::Convert::Paragraph::end($paragraph->{'container'}));
3220    if ($self->{'context'}->[-1] eq 'flushright') {
3221      $result = $self->_align_environment($result,
3222        $self->{'text_element_context'}->[-1]->{'max'}, 'right');
3223    }
3224    pop @{$self->{'formatters'}};
3225    delete $self->{'text_element_context'}->[-1]->{'counter'};
3226  } elsif ($preformatted) {
3227    $result .= _count_added($self, $preformatted->{'container'},
3228               Texinfo::Convert::Paragraph::end($preformatted->{'container'}));
3229    if ($result ne '') {
3230      $result = $self->ensure_end_of_line($result);
3231    }
3232    if ($self->{'context'}->[-1] eq 'flushright') {
3233      $result = $self->_align_environment ($result,
3234        $self->{'text_element_context'}->[-1]->{'max'}, 'right');
3235    }
3236    pop @{$self->{'formatters'}};
3237    # We assume that, upon closing the preformatted we are at the
3238    # beginning of a line.
3239    delete $self->{'text_element_context'}->[-1]->{'counter'};
3240  }
3241
3242  # close commands
3243  if ($command) {
3244    if ($command eq 'float') {
3245      if ($root->{'extra'}
3246          and ($root->{'extra'}->{'type'}->{'normalized'} ne ''
3247               or defined($root->{'number'})
3248               or $root->{'extra'}->{'caption'} or $root->{'extra'}->{'shortcaption'})) {
3249
3250        $result .= _add_newline_if_needed($self);
3251        my ($caption, $prepended) = Texinfo::Common::float_name_caption($self,
3252                                                                        $root);
3253        if ($prepended) {
3254          $prepended->{'type'} = 'frenchspacing';
3255          my $float_number = $self->convert_line ($prepended);
3256          $result .= $float_number;
3257          $self->{'text_element_context'}->[-1]->{'counter'} +=
3258            Texinfo::Convert::Unicode::string_width($float_number);
3259          $self->{'empty_lines_count'} = 0;
3260        }
3261        if ($caption) {
3262          $self->{'format_context'}->[-1]->{'paragraph_count'} = 0;
3263          my $tree = $caption->{'args'}->[0];
3264          $result .= _convert($self, $tree);
3265        }
3266      }
3267    } elsif (($command eq 'quotation'
3268               or $command eq 'smallquotation')
3269             and $root->{'extra'} and $root->{'extra'}->{'authors'}) {
3270      foreach my $author (@{$root->{'extra'}->{'authors'}}) {
3271        $result .= _convert($self,
3272                 $self->gdt("\@center --- \@emph{{author}}\n",
3273                    {'author' => $author->{'args'}->[0]->{'contents'}}));
3274      }
3275    } elsif (($command eq 'multitable')) {
3276      $self->{'document_context'}->[-1]->{'in_multitable'}--;
3277    } elsif ($root_commands{$command}
3278        and $sectioning_commands{$command}
3279        and $command ne 'part') {
3280      # add menu if missing
3281      my $node = $self->{'node'};
3282      my $automatic_directions;
3283      if ($node and defined($node->{'extra'}->{'nodes_manuals'})) {
3284        $automatic_directions =
3285            (scalar(@{$node->{'extra'}->{'nodes_manuals'}}) == 1);
3286      }
3287      if ($node and $automatic_directions
3288            and !$self->{'seenmenus'}->{$node}) {
3289        $self->{'seenmenus'}->{$node} = 1;
3290        my $menu_node = Texinfo::Structuring::new_complete_node_menu(undef, $node);
3291        if ($menu_node) {
3292          my $menu_text = $self->_convert($menu_node);
3293          if ($menu_text) {
3294            $result .= $menu_text;
3295          }
3296        }
3297      }
3298    }
3299
3300    # close the contexts and register the cells
3301    if ($self->{'preformatted_context_commands'}->{$command}
3302        or $command eq 'float') {
3303      my $old_context = pop @{$self->{'context'}};
3304      die "Not a preformatted context: $old_context"
3305        if (!$self->{'preformatted_context_commands'}->{$old_context}
3306            and $old_context ne 'float');
3307      if ($old_context ne 'float' and !$menu_commands{$old_context}) {
3308        $self->{'empty_lines_count'} = 0;
3309      }
3310      delete ($self->{'preformatted_context_commands'}->{$command})
3311       unless ($default_preformatted_context_commands{$command});
3312    } elsif ($flush_commands{$command}) {
3313      my $old_context = pop @{$self->{'context'}};
3314      die if (! $flush_commands{$old_context});
3315    }
3316
3317    if ($self->{'format_context_commands'}->{$command}) {
3318      pop @{$self->{'format_context'}};
3319      delete ($self->{'format_context_commands'}->{$command})
3320       unless ($default_format_context_commands{$command});
3321    } elsif ($cell) {
3322      pop @{$self->{'format_context'}};
3323      pop @{$self->{'text_element_context'}};
3324      push @{$self->{'format_context'}->[-1]->{'row'}}, $result;
3325      _update_count_context($self);
3326      my $cell_counts = pop @{$self->{'count_context'}};
3327      push @{$self->{'format_context'}->[-1]->{'row_counts'}}, $cell_counts;
3328      $result = '';
3329    }
3330    if ($advance_paragraph_count_commands{$command}) {
3331      $self->{'format_context'}->[-1]->{'paragraph_count'}++;
3332    }
3333  }
3334
3335  return $result;
3336}
3337
33381;
3339
3340__END__
3341# $Id$
3342# Automatically generated from maintain/template.pod
3343
3344=head1 NAME
3345
3346Texinfo::Convert::Plaintext - Convert Texinfo tree to Plaintext
3347
3348=head1 SYNOPSIS
3349
3350  my $converter
3351    = Texinfo::Convert::Plaintext->converter({'parser' => $parser});
3352
3353  $converter->output($tree);
3354  $converter->convert($tree);
3355  $converter->convert_tree($tree);
3356
3357=head1 DESCRIPTION
3358
3359Texinfo::Convert::Plaintext converts a Texinfo tree to Plaintext.
3360
3361=head1 METHODS
3362
3363=over
3364
3365=item $converter = Texinfo::Convert::Plaintext->converter($options)
3366
3367Initialize converter from Texinfo to Plaintext.
3368
3369The I<$options> hash reference holds options for the converter.  In
3370this option hash reference a parser object may be associated with the
3371I<parser> key.  The other options should be configuration options
3372described in the Texinfo manual.  Those options, when appropriate,
3373override the document content.
3374
3375See L<Texinfo::Convert::Converter> for more informations.
3376
3377=item $converter->output($tree)
3378
3379Convert a Texinfo tree I<$tree> and output the result in files as
3380described in the Texinfo manual.
3381
3382=item $result = $converter->convert($tree)
3383
3384Convert a Texinfo tree I<$tree> or tree portion and return
3385the resulting output.
3386
3387=item $result = $converter->convert_tree($tree)
3388
3389Convert a Texinfo tree portion I<$tree> and return the resulting
3390output.  This function does not try to output a full document but only
3391portions.  For a full document use C<convert>.
3392
3393=back
3394
3395=head1 AUTHOR
3396
3397Patrice Dumas, E<lt>pertusus@free.frE<gt>
3398
3399=cut
3400