# Converter.pm: Common code for Converters.
#
# Copyright 2011-2024 Free Software Foundation, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License,
# or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# Original author: Patrice Dumas
package Texinfo::Convert::Converter;
use 5.006;
use strict;
# To check if there is no erroneous autovivification
#no autovivification qw(fetch delete exists store strict);
# for fileparse
use File::Basename;
# for file names portability
use File::Spec;
use Encode qw(decode);
# for dclone
use Storable;
#use Data::Dumper;
use Carp qw(cluck confess);
use Texinfo::Convert::ConvertXS;
use Texinfo::XSLoader;
use Texinfo::Options;
use Texinfo::Common;
use Texinfo::Report;
use Texinfo::Convert::Utils;
use Texinfo::Convert::Unicode;
use Texinfo::Convert::Texinfo;
use Texinfo::Convert::Text;
use Texinfo::Convert::NodeNameNormalization;
use Texinfo::OutputUnits;
use Texinfo::Translations;
require Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(
xml_protect_text
xml_comment
xml_accent
xml_accents
);
our $VERSION = '7.2';
my $XS_convert = Texinfo::XSLoader::XS_convert_enabled();
our $module_loaded = 0;
my %XS_overrides = (
# fully overriden for all the converters
"Texinfo::Convert::Converter::_generic_converter_init",
=> "Texinfo::Convert::ConvertXS::generic_converter_init",
"Texinfo::Convert::Converter::set_document"
=> "Texinfo::Convert::ConvertXS::converter_set_document",
"Texinfo::Convert::Converter::set_conf"
=> "Texinfo::Convert::ConvertXS::set_conf",
"Texinfo::Convert::Converter::force_conf"
=> "Texinfo::Convert::ConvertXS::force_conf",
"Texinfo::Convert::Converter::get_conf"
=> "Texinfo::Convert::ConvertXS::get_conf",
"Texinfo::Convert::Converter::get_converter_errors"
=> "Texinfo::Convert::ConvertXS::get_converter_errors",
"Texinfo::Convert::Converter::converter_line_error"
=> "Texinfo::Convert::ConvertXS::converter_line_error",
"Texinfo::Convert::Converter::converter_line_warn"
=> "Texinfo::Convert::ConvertXS::converter_line_warn",
"Texinfo::Convert::Converter::converter_document_error"
=> "Texinfo::Convert::ConvertXS::converter_document_error",
"Texinfo::Convert::Converter::converter_document_warn"
=> "Texinfo::Convert::ConvertXS::converter_document_warn",
"Texinfo::Convert::Converter::get_converter_indices_sorted_by_letter"
=> "Texinfo::Convert::ConvertXS::get_converter_indices_sorted_by_letter",
"Texinfo::Convert::Converter::get_converter_indices_sorted_by_index"
=> "Texinfo::Convert::ConvertXS::get_converter_indices_sorted_by_index",
"Texinfo::Convert::Converter::set_global_document_commands"
=> "Texinfo::Convert::ConvertXS::converter_set_global_document_commands",
# XS only
"Texinfo::Convert::Converter::reset_converter"
=> "Texinfo::Convert::ConvertXS::reset_converter",
"Texinfo::Convert::Converter::destroy"
=> "Texinfo::Convert::ConvertXS::destroy",
"Texinfo::Convert::Converter::XS_get_unclosed_stream"
=> "Texinfo::Convert::ConvertXS::get_unclosed_stream",
);
sub import {
if (!$module_loaded) {
if ($XS_convert) {
foreach my $sub (keys %XS_overrides) {
Texinfo::XSLoader::override ($sub, $XS_overrides{$sub});
}
}
$module_loaded = 1;
}
# The usual import method
goto &Exporter::import;
}
# values for integer and string options in code generated from
# Texinfo/Convert/converters_defaults.txt
my $regular_defaults
= Texinfo::Options::get_converter_regular_options('converter');
my %defaults = %$regular_defaults;
# values for integer and string options in code generated from
# Texinfo/Convert/converters_defaults.txt
my $common_defaults = Texinfo::Options::get_converter_regular_options('common');
# defaults for all converters that are not defined elsewhere.
# undef values in general marks information passed by the caller that
# is valid in the initialization hash, but is not considered
# as "configuration/customization". It is not available through get_conf()
# but is available directly in the converter as a hash key.
# FIXME separate the two types of information and check that those
# items are not valid customization options? Or make them customization
# variables that can only be set from init files, like buttons or icons?
# NOTE converters for now only set customization variables.
# It would be good to keep it that way.
my %common_converters_defaults = (
# Following are set in the main program
'deprecated_config_directories' => undef,
# Not set in the main program
'translated_commands' => {'error' => 'error@arrow{}',},
# integer and string customization variables common for all the converters
# with values different from main program values
%$common_defaults
);
my %all_converters_defaults
= (%Texinfo::Options::converter_cmdline_options,
%Texinfo::Options::converter_customization_options,
%Texinfo::Options::unique_at_command_options,
%Texinfo::Options::multiple_at_command_options,
%common_converters_defaults
);
# For translation of in document string.
if (0) {
my $self;
# TRANSLATORS: expansion of @error{} as Texinfo code
$self->cdt('error@arrow{}');
}
our %default_args_code_style = (
'email' => [1],
'anchor' => [1],
'uref' => [1],
'url' => [1],
'math' => [1],
'inforef' => [1,undef,1],
'image' => [1, 1, 1, undef, 1],
# and type?
'float' => [1],
);
foreach my $code_style_command (keys(%Texinfo::Commands::brace_code_commands)) {
$default_args_code_style{$code_style_command} = [1];
}
foreach my $ref_cmd ('pxref', 'xref', 'ref') {
$default_args_code_style{$ref_cmd} = [1, undef, undef, 1];
}
################################################################
# converter API
# convert_tree() and convert() should be implemented in converters.
# There is an implementation of output() below but in general
# output() should also be implemented by Converters. The simple
# implementation of convert_output_unit() below is likely to be
# ok for most converters.
# Functions that should be defined in specific converters
sub converter_defaults($$)
{
return \%defaults;
}
# should be redefined by specific converters
sub converter_initialize($)
{
}
sub conversion_initialization($;$)
{
my $converter = shift;
my $document = shift;
if ($document) {
$converter->set_document($document);
}
}
sub conversion_finalization($)
{
#my $converter = shift;
}
sub output_internal_links($)
{
my $self = shift;
return undef;
}
sub set_document($$)
{
my $converter = shift;
my $document = shift;
$converter->{'document'} = $document;
Texinfo::Common::set_output_encoding($converter, $document);
$converter->{'convert_text_options'}
= Texinfo::Convert::Text::copy_options_for_convert_text($converter);
# In general, OUTPUT_PERL_ENCODING set below is needed for the output()
# entry point through Texinfo::Convert::Utils::output_files_open_out. It is
# also sometime needed for the converter itself. If not, in general it
# is not needed for the convert() entry point, so the call could also be
# done more finely in converters, but it is not really important.
Texinfo::Common::set_output_perl_encoding($converter);
}
# initialization either in generic XS converter or in Perl
sub _generic_converter_init($$;$)
{
my $converter = shift;
my $format_defaults = shift;
my $conf = shift;
my %defaults = %all_converters_defaults;
if ($format_defaults) {
foreach my $key (keys(%$format_defaults)) {
$defaults{$key} = $format_defaults->{$key};
}
}
$converter->{'conf'} = {};
foreach my $key (keys(%defaults)) {
if (Texinfo::Common::valid_customization_option($key)) {
$converter->{'conf'}->{$key} = $defaults{$key};
} else {
$converter->{$key} = $defaults{$key};
}
}
$converter->{'configured'} = {};
if (defined($conf)) {
foreach my $key (keys(%$conf)) {
if (Texinfo::Common::valid_customization_option($key)) {
$converter->{'conf'}->{$key} = $conf->{$key};
} elsif (!exists($defaults{$key})) {
my $class = ref($converter);
warn "$class: $key not a possible configuration\n";
} else {
$converter->{$key} = $conf->{$key};
}
# configuration set here, from the argument of the converter,
# in general coming from command-line or from init files will not
# be reset by set_conf.
$converter->{'configured'}->{$key} = 1;
}
}
# set $converter->{'converter_init_conf'} to the customization
# options obtained after setting the defaults and applying
# the customization passed as argument.
$converter->{'converter_init_conf'} = { %{$converter->{'conf'}} };
# used for output files information, to register opened
# and not closed files. Accessed through output_files_information()
$converter->{'output_files'}
= Texinfo::Convert::Utils::output_files_initialize();
# setup expanded formats as a hash.
my $expanded_formats = $converter->{'conf'}->{'EXPANDED_FORMATS'};
$converter->{'expanded_formats'} = {};
if (defined($expanded_formats)) {
foreach my $expanded_format (@$expanded_formats) {
$converter->{'expanded_formats'}->{$expanded_format} = 1;
}
}
$converter->{'error_warning_messages'} = [];
}
# this function is designed so as to be used in specific Converters
# and not redefined.
sub converter($;$)
{
my $class = shift;
my $conf = shift;
my $converter = {};
bless $converter, $class;
my $format_defaults = $converter->converter_defaults($conf);
_generic_converter_init($converter, $format_defaults, $conf);
$converter->converter_initialize();
return $converter;
}
sub convert_output_unit($$)
{
my $self = shift;
my $output_unit = shift;
my $result = '';
foreach my $element (@{$output_unit->{'unit_contents'}}) {
$result .= $self->convert_tree($element);
}
return $result;
}
# should be redefined by specific converters
sub conversion_output_begin($;$$)
{
my $self = shift;
my $output_file = shift;
my $output_filename = shift;
return '';
}
sub conversion_output_end($)
{
my $self = shift;
return '';
}
sub output_tree($$)
{
my $self = shift;
my $document = shift;
$self->conversion_initialization($document);
my $root = $document->tree();
my ($output_file, $destination_directory, $output_filename)
= $self->determine_files_and_directory(
$self->get_conf('TEXINFO_OUTPUT_FORMAT'));
my ($encoded_destination_directory, $dir_encoding)
= $self->encoded_output_file_name($destination_directory);
my $succeeded
= $self->create_destination_directory($encoded_destination_directory,
$destination_directory);
unless ($succeeded) {
$self->conversion_finalization();
return undef;
}
my $fh;
my $encoded_output_file;
if (! $output_file eq '') {
my $path_encoding;
($encoded_output_file, $path_encoding)
= $self->encoded_output_file_name($output_file);
my $error_message;
# the third return information, set if the file has already been used
# in this files_information is not checked as this cannot happen.
($fh, $error_message) = Texinfo::Convert::Utils::output_files_open_out(
$self->output_files_information(), $self,
$encoded_output_file);
if (!$fh) {
$self->converter_document_error(
sprintf(__("could not open %s for writing: %s"),
$output_file, $error_message));
$self->conversion_finalization();
return undef;
}
}
my $output_beginning
= $self->conversion_output_begin($output_file, $output_filename);
my $result = '';
$result .= $self->write_or_return($output_beginning, $fh);
$result .= $self->write_or_return($self->convert_tree($root), $fh);
my $output_end = $self->conversion_output_end();
$result .= $self->write_or_return($output_end, $fh);
if ($fh and $output_file ne '-') {
Texinfo::Convert::Utils::output_files_register_closed(
$self->output_files_information(), $encoded_output_file);
if (!close ($fh)) {
$self->converter_document_error(
sprintf(__("error on closing %s: %s"),
$output_file, $!));
}
}
$self->conversion_finalization();
return $result;
}
# Nothing to do in Perl. XS function resets converter
sub reset_converter($)
{
}
# Nothing to do in Perl. XS function frees memory
sub destroy($)
{
}
sub XS_get_unclosed_stream($$)
{
return undef;
}
sub output_files_information($)
{
my $self = shift;
return $self->{'output_files'};
}