blob: 6cacea9f86aadb1b517ae209762ff7e0b7754fec [file]
# ConverterNonXS.pm: Common code for Converters.
#
# Copyright 2011-2026 Free Software Foundation, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License,
# or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Original author: Patrice Dumas <pertusus@free.fr>
# ALTIMP perl/XSTexinfo/convert/ConvertConverter.xs
# ALTIMP C/convert/converter.c
package Texinfo::Convert::Converter;
use 5.006;
use strict;
use warnings;
# To check if there is no erroneous autovivification
#no autovivification qw(fetch delete exists store strict);
#use Data::Dumper;
use Carp qw(cluck confess);
eval { require Devel::Cycle; Devel::Cycle->import(); };
use Texinfo::Options;
use Texinfo::Common;
use Texinfo::Report;
use Texinfo::Document;
use Texinfo::Convert::Utils;
use Texinfo::Convert::Text;
# values for integer and string options in code generated from
# Texinfo/Convert/converters_defaults.txt
# customization variables defaults for all converters that are
# not defined elsewhere.
# Integer and string customization variables common for all the converters
# with values different from main program values
my $converter_common_defaults
= Texinfo::Options::get_regular_options('converter_common');
# Non-customization variables defaults for all converters.
# undef values in general mark information passed by the caller that
# is valid here. These defaults are not considered as
# "configuration/customization" and therefore are not available through
# get_conf(), but are available directly in the converter as a hash key.
# TODO check that those items are not valid customization options?
# TODO make those items customization variables that can only be set
# from init files, like buttons or icons?
# NOTE converters should never set those items.
my %common_converters_non_options_defaults = (
# Following are set in the main program
'deprecated_config_directories' => undef,
# Not set in the main program
'translated_commands' => {'error' => 'error@arrow{}',},
);
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,
%$converter_common_defaults,
%common_converters_non_options_defaults
);
################################################################
# converter API
sub set_document($$) {
my ($converter, $document) = @_;
if (defined($document)) {
$converter->{'document'} = $document;
Texinfo::Common::set_output_encoding($converter, $document);
} else {
delete $converter->{'document'};
}
$converter->{'convert_text_options'}
= Texinfo::Convert::Text::copy_options_for_convert_text($converter);
delete $converter->{'sorted_indices_by_letter'};
delete $converter->{'sorted_indices_by_index'};
delete $converter->{'index_entries_sort_strings'};
}
# initialization either in generic XS converter or in Perl
sub _generic_converter_init($$;$) {
my ($converter, $format_defaults, $conf) = @_;
my %defaults = %all_converters_defaults;
$converter->{'commands_init_conf'} = {};
if (defined($format_defaults)) {
foreach my $key (keys(%$format_defaults)) {
$defaults{$key} = $format_defaults->{$key};
if (exists($Texinfo::Common::document_settable_at_commands{$key})) {
$converter->{'commands_init_conf'}->{$key} = $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'} = {};
# customization options obtained from command-line for @-commands.
if (defined($conf)) {
foreach my $key (keys(%$conf)) {
if (Texinfo::Common::valid_customization_option($key)) {
$converter->{'conf'}->{$key} = $conf->{$key};
if (exists($Texinfo::Common::document_settable_at_commands{$key})) {
$converter->{'commands_init_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;
}
if (exists($conf->{'documentlanguage'})) {
$converter->converter_set_documentlanguage($conf->{'documentlanguage'});
}
if (exists($conf->{'documentscript'})) {
$converter->converter_set_documentscript($conf->{'documentscript'});
}
}
# 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.
$converter->{'expanded_formats'} = {};
if (exists($converter->{'conf'}->{'EXPANDED_FORMATS'})) {
my $expanded_formats = $converter->{'conf'}->{'EXPANDED_FORMATS'};
foreach my $expanded_format (@$expanded_formats) {
$converter->{'expanded_formats'}->{$expanded_format} = 1;
}
}
$converter->{'error_warning_messages'} = [];
}
# ALTIMP convert/texinfo.c txi_converter_remove_output_units
sub converter_remove_output_units($) {
my $self = shift;
$self->perl_converter_remove_output_units();
}
# ALTIMP convert/texinfo.c txi_destroy_converter
# convert/converter.c destroy_converter
# Has an XS override, that calls the converter_perl_release Perl function
# in addition to C code.
sub destroy_converter($) {
my $self = shift;
$self->converter_perl_release();
#find_cycle($self);
}
sub XS_get_unclosed_stream($$) {
return undef;
}
# returns main output units list, special output units list and associated
# output units lists, for output units managed in C/XS only. Not generally
# needed, as all the computations are done through XS, but can be useful
# for debugging.
# (Access of pure Perl converters output units lists should be through
# get_output_units_lists)
sub XS_get_output_units_lists($) {
return (undef, undef, undef);
}
# errors and warnings
sub converter_line_error($$$;$) {
my ($self, $text, $error_location_info, $continuation) = @_;
my $message = Texinfo::Report::format_line_message('error', $text,
$error_location_info, $continuation,
$self->get_conf('DEBUG'));
push @{$self->{'error_warning_messages'}}, $message;
}
sub converter_line_warn($$$;$) {
my ($self, $text, $error_location_info, $continuation) = @_;
my $message = Texinfo::Report::format_line_message('warning', $text,
$error_location_info, $continuation,
$self->get_conf('DEBUG'));
push @{$self->{'error_warning_messages'}}, $message;
}
sub converter_document_error($$;$) {
my ($self, $text, $continuation) = @_;
my $program_name;
if ($self->get_conf('PROGRAM') && $self->get_conf('PROGRAM') ne '') {
$program_name = $self->get_conf('PROGRAM');
}
my $message
= Texinfo::Report::format_document_message('error', $text, $program_name,
$continuation);
push @{$self->{'error_warning_messages'}}, $message;
}
sub converter_document_warn($$;$) {
my ($self, $text, $continuation) = @_;
my $program_name;
if ($self->get_conf('PROGRAM') && $self->get_conf('PROGRAM') ne '') {
$program_name = $self->get_conf('PROGRAM');
}
my $message
= Texinfo::Report::format_document_message('warning', $text,
$program_name, $continuation);
push @{$self->{'error_warning_messages'}}, $message;
}
sub get_converter_errors($) {
my $self = shift;
my $errors = $self->{'error_warning_messages'};
$self->{'error_warning_messages'} = [];
return $errors;
}
sub merge_converter_error_messages_lists($$) {
my ($dst, $src) = @_;
if (!defined($src) or !exists($src->{'error_warning_messages'})) {
return;
}
push @{$dst->{'error_warning_messages'}},
splice(@{$src->{'error_warning_messages'}});
}
###############################################################
# Implementation of the customization API that is used in many
# Texinfo modules
# Unknown variables can only happen when called from init files. From
# command-line checks are done before.
sub get_conf($$) {
my ($self, $var_name) = @_;
if (!Texinfo::Common::valid_customization_option($var_name)) {
$self->converter_document_error(sprintf(__(
"unknown customization variable: %s"),
$var_name));
if ($self->{'conf'}->{'TEST'}) {
cluck ("BUG: get_conf: unknown customization variable: $var_name");
}
return undef;
}
return $self->{'conf'}->{$var_name};
}
sub set_conf($$$) {
my ($self, $var_name, $value) = @_;
if (!Texinfo::Common::valid_customization_option($var_name)) {
$self->converter_document_error(sprintf(__(
"unknown customization variable: %s"),
$var_name));
if ($self->{'conf'}->{'TEST'}) {
cluck ("BUG: set_conf: unknown customization variable: $var_name");
}
return 0;
}
if ($self->{'configured'}->{$var_name}) {
return 0;
} else {
$self->{'conf'}->{$var_name} = $value;
return 1;
}
}
sub force_conf($$$) {
my ($self, $var_name, $value) = @_;
if (!Texinfo::Common::valid_customization_option($var_name)) {
$self->converter_document_error(sprintf(__(
"unknown customization variable: %s"),
$var_name));
if ($self->{'conf'}->{'TEST'}) {
cluck ("BUG: force_conf: unknown customization variable: $var_name");
}
return 0;
}
$self->{'conf'}->{$var_name} = $value;
return 1;
}
# $COMMANDS_LOCATION is 'before', 'last', 'preamble' or 'preamble_or_first'
# 'before' means setting to the values before the document commands
# (defaults and command-line).
# 'preamble' means setting sequentially to the values in the preamble.
# 'preamble_or_first' means setting to the first value for the command
# in the document if the first command is not in the preamble, else set
# sequentially to the values in the preamble.
# 'last' means setting to the last value for the command in the document.
#
# Notice that the only effect is to use set_conf (directly or through
# set_global_document_command), no @-commands setting side effects are done
# and associated customization variables are not set/reset either.
sub set_global_document_commands($$$) {
my ($self, $commands_location, $selected_commands) = @_;
my $init_conf = $self->{'commands_init_conf'};
if (not defined($selected_commands)) {
die "set_global_document_commands: requires selected commands";
}
if ($commands_location eq 'before') {
foreach my $global_command (@{$selected_commands}) {
# for commands not appearing in the document, this should set to
# the converter initialization value, which is in init_conf,
# or generic default
my $conf_value = get_command_init($global_command, $init_conf);
$self->set_conf($global_command, $conf_value);
# NOTE if the variable is set from an handler, or in the converter after
# $init_conf was set, but before starting the conversion, it is ignored
# here and the $init_conf value is set. The previously set value
# could be in $self->get_conf(), but what is available from
# $self->get_conf() could also be a value set by a previous call of
# set_global_document_commands.
# There is no easy way to deal with this issue, other than making sure
# that a customization value that is expected to be set early is set in
# $init_conf.
}
} else {
my $global_commands;
if (exists($self->{'document'})) {
$global_commands = $self->{'document'}->global_commands_information();
}
foreach my $global_command (@{$selected_commands}) {
if ($self->get_conf('DEBUG')) {
print STDERR "SET_global($commands_location) $global_command\n";
}
my $element;
if (defined($global_commands)) {
$element = Texinfo::Common::set_global_document_command($self,
$global_commands, $global_command, $commands_location);
}
if (not defined($element)) {
# for commands not appearing in the document, this should set to
# the converter initialization value, which is in init_conf,
# or generic default
# the NOTE above in 'before' holds here too.
$self->set_conf($global_command,
get_command_init($global_command, $init_conf));
}
}
}
}
# Specific elements formatting and other helper functions
# ALTIMP partial in main/targets.c
# For user-defined HTML customization, documented in the specific manual.
# Return the element in the tree that $IDENTIFIER refers to.
sub converter_find_identifier_target($$) {
my ($self, $identifier) = @_;
if (!defined($identifier)) {
cluck;
}
my $identifiers_target;
if (exists($self->{'document'})) {
$identifiers_target = $self->{'document'}->labels_information();
if (defined($identifiers_target)) {
return $identifiers_target->{$identifier};
}
}
return undef;
}
sub get_converter_indices_sorted_by_letter($) {
my $self = shift;
my $indices_information;
if (exists($self->{'document'})) {
$indices_information = $self->{'document'}->indices_information();
if (defined($indices_information)) {
my $use_unicode_collation
= $self->get_conf('USE_UNICODE_COLLATION');
my $lang_sorting_locale;
if (!(defined($use_unicode_collation) and !$use_unicode_collation)) {
$lang_sorting_locale = $self->get_conf('COLLATION_LANGUAGE');
if (!defined($lang_sorting_locale)
and $self->get_conf('DOCUMENTLANGUAGE_COLLATION')
and defined($self->{'current_lang_translations'})) {
my $lang_info = $self->{'current_lang_translations'}->[0];
if (exists($lang_info->{'lang'})) {
# use a name expected by Unicode::Collate::Locale
$lang_sorting_locale = $lang_info->{'lang'};
if (exists($lang_info->{'script'})) {
$lang_sorting_locale .= '_'.$lang_info->{'script'};
}
}
}
}
return _converter_sorted_indices_by_letter($self,
$use_unicode_collation, $lang_sorting_locale);
}
}
return undef;
}
sub get_converter_indices_sorted_by_index($) {
my $self = shift;
my $indices_information;
if (exists($self->{'document'})) {
$indices_information = $self->{'document'}->indices_information();
if (defined($indices_information)) {
my $use_unicode_collation
= $self->get_conf('USE_UNICODE_COLLATION');
my $lang_sorting_locale;
if (!(defined($use_unicode_collation) and !$use_unicode_collation)) {
$lang_sorting_locale = $self->get_conf('COLLATION_LANGUAGE');
if (!defined($lang_sorting_locale)
and $self->get_conf('DOCUMENTLANGUAGE_COLLATION')
and defined($self->{'current_lang_translations'})) {
my $lang_info = $self->{'current_lang_translations'}->[0];
if (exists($lang_info->{'lang'})) {
# use a name expected by Unicode::Collate::Locale
$lang_sorting_locale = $lang_info->{'lang'};
if (exists($lang_info->{'script'})) {
$lang_sorting_locale .= '_'.$lang_info->{'script'};
}
}
}
}
return _converter_sorted_indices_by_index($self,
$use_unicode_collation, $lang_sorting_locale);
}
}
return undef;
}
1;