# Config.pm: namespace used for user configuration (init files) evaluation
#
# Copyright 2010-2023 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
#
# functions that should not be called by user init files codes, but
# are called by the main program are prefixed by GNUT_ while functions that can
# be called by user init files codes are prefixed by texinfo_.
# Functions prefixed by _GNUT_ are private.
#
# This package is documented in the customization_api Texinfo manual. This
# package is integrated with the Texinfo main program and the Texinfo
# HTML converter, such that it does not make sense to document the
# public interface separately.
package Texinfo::Config;
use strict;
# To check if there is no erroneous autovivification
#no autovivification qw(fetch delete exists store strict);
use Carp qw(carp confess);
# for Encode::encode
use Encode;
# for __( and __p( and some functions
use Texinfo::Common;
# for error messages, passed from main program through initialization
# function.
my $real_command_name = '';
#####################################################################
# customization API, used from main program and from init files
my $cmdline_options;
my $main_program_default_options;
my $init_files_options = {};
# List options that can be set from main program are not
# handled in the same way than string options. Indeed, the
# lists need to be defined in the main program, therefore the main
# program list options would always take precedence
# if there is a precedence, and the list options set from
# init file would never have any effect.
# Therefore, for list options, items are added and removed by
# calls to texinfo_add_to_option_list
# and texinfo_remove_from_option_list, be it from command line
# or init files, there is no precedence, but the order of calls
# matter.
my %options_as_lists;
# called from texi2any.pl main program and t/test_utils.pl.
sub GNUT_initialize_config($$$) {
$real_command_name = shift;
$main_program_default_options = shift;
$cmdline_options = shift;
# consider options passed from main program for initialization
# as list options
foreach my $cmdline_option (keys(%$cmdline_options)) {
if (ref($cmdline_options->{$cmdline_option}) eq ''
or ref($cmdline_options->{$cmdline_option}) ne 'ARRAY') {
warn "BUG: $cmdline_option not an ARRAY $cmdline_options->{$cmdline_option}\n";
}
$options_as_lists{$cmdline_option} = 1;
}
#print STDERR "cmdline_options: ".join('|',keys(%$cmdline_options))."\n";
return $init_files_options;
}
# duplicated from texi2any.pl
sub _GNUT_encode_message($)
{
my $text = shift;
my $encoding = texinfo_get_conf('MESSAGE_ENCODING');
if (defined($encoding)) {
return Encode::encode($encoding, $text);
} else {
return $text;
}
}
# duplicated from texi2any.pl
sub _GNUT_decode_input($)
{
my $text = shift;
my $encoding = texinfo_get_conf('COMMAND_LINE_ENCODING');
if (defined($encoding)) {
return decode($encoding, $text);
} else {
return $text;
}
}
# duplicated from texi2any.pl
sub _GNUT_document_warn($) {
return if (texinfo_get_conf('NO_WARN'));
my $text = shift;
chomp ($text);
warn(_GNUT_encode_message(
sprintf(__p("program name: warning: warning_message",
"%s: warning: %s"), $real_command_name, $text)."\n"));
}
sub _GNUT_document_fatal($) {
my $text = shift;
chomp ($text);
warn(_GNUT_encode_message(
sprintf(__p("program name: error_message",
"%s: %s"), $real_command_name, $text)."\n"));
exit 1 unless (texinfo_get_conf('FORCE'));
}
# used to register messages by the user with texinfo_register_init_loading_*
my @init_file_loading_messages;
# called from texi2any.pl main program and t/test_utils.pl.
# eval $FILE in the Texinfo::Config namespace. $FILE should be a binary string.
sub GNUT_load_init_file($) {
my $file = shift;
push @init_file_loading_messages, [];
my $result = do($file);
my $message = $@;
my $read_error = $!;
if (!defined($result)) {
if (defined($message) and $message ne '') {
_GNUT_document_fatal(sprintf
(__("error parsing %s: %s"),
_GNUT_decode_input($file), $message));
} elsif (defined($read_error)) {
_GNUT_document_fatal(sprintf
(__("error reading %s: %s"),
_GNUT_decode_input($file), $read_error));
}
}
# Note: $message or $read_error may be incorrectly "double encoded" if they
# are encoded byte strings. However, it appears that they are unencoded
# character strings if the init file uses the "use utf8" pragma to mark the
# file as UTF-8 encoded, which may become the default in the future according
# to the Perl documentation.
my $file_loading_messages = pop @init_file_loading_messages;
my $error_nr = 0;
for my $error (@{$file_loading_messages}) {
my $type = $error->{'type'};
my $message = $error->{'text'};
chomp($message);
if ($type eq 'error') {
$error_nr += 1;
warn(_GNUT_encode_message(
sprintf(__p("init file: error_message",
"%s: %s"),
_GNUT_decode_input($file), $message)."\n"));
} else {
if (not texinfo_get_conf('NO_WARN')) {
warn(_GNUT_encode_message(
sprintf(__p("init file: warning: warning_message",
"%s: warning: %s"),
_GNUT_decode_input($file), $message)."\n"));
}
}
}
if ($error_nr > 0 and !texinfo_get_conf('FORCE')) {
exit 1;
}
}
# called from init files in case of errors at loading.
sub texinfo_register_init_loading_error($) {
my $message = shift;
push @{$init_file_loading_messages[-1]}, {'type' => 'error',
'text' => $message};
}
# called from init files in case of warnings at loading.
sub texinfo_register_init_loading_warning($) {
my $message = shift;
push @{$init_file_loading_messages[-1]}, {'type' => 'warning',
'text' => $message};
}
# L2H removed in 2021
# return undef var when there is nothing to set.
sub _GNUT_map_obsolete_options($$)
{
my $input_var = shift;
my $input_value = shift;
my $var = $input_var;
my $value = $input_value;
if ($input_var eq 'L2H') {
_GNUT_document_warn(sprintf(__("obsolete option: %s"), $input_var));
if (! $input_value) {
# nothing to do in that case
$var = undef;
$value = undef;
} else {
$var = 'HTML_MATH';
$value = 'l2h';
}
}
return $var, $value;
}
# Called from init files to set configuration options.
sub texinfo_set_from_init_file($$) {
my $var = shift;
my $value = shift;
($var, $value) = _GNUT_map_obsolete_options($var, $value);
if (!defined($var)) {
return 1;
}
if (!Texinfo::Common::valid_customization_option($var)) {
# carp may be better, but infortunately, it points to the routine
# that loads the file, and not to the init file.
_GNUT_document_warn(sprintf(__("%s: unknown variable %s"),
'texinfo_set_from_init_file', $var));
return 0;
}
return 0 if (defined($cmdline_options->{$var}));
delete $main_program_default_options->{$var};
$init_files_options->{$var} = $value;
return 1;
}
# set option from the command line, called from main program.
# Highest precedence.
sub GNUT_set_from_cmdline($$)
{
my $var = shift;
my $value = shift;
($var, $value) = _GNUT_map_obsolete_options($var, $value);
if (!defined($var)) {
return 1;
}
delete $init_files_options->{$var};
delete $main_program_default_options->{$var};
if (!Texinfo::Common::valid_customization_option($var)) {
_GNUT_document_warn(sprintf(__("unknown variable from command line: %s\n"),
$var));
return 0;
}
$cmdline_options->{$var} = $value;
return 1;
}
# add default based, for instance, on the format.
sub GNUT_set_main_program_default($$)
{
my $var = shift;
my $value = shift;
($var, $value) = _GNUT_map_obsolete_options($var, $value);
if (!defined($var)) {
return 1;
}
return 0 if (defined($cmdline_options->{$var})
or defined($init_files_options->{$var}));
$main_program_default_options->{$var} = $value;
return 1;
}
# called both from main program and init files, for %options_as_lists
# options with lists set in main program.
sub texinfo_add_to_option_list($$)
{
my $var = shift;
my $values_array_ref = shift;
if (not $options_as_lists{$var}) {
return 0;
}
foreach my $value (@$values_array_ref) {
push @{$cmdline_options->{$var}}, $value
unless (grep {$_ eq $value} @{$cmdline_options->{$var}});
}
return 1;
}
# called both from main program and init files.
sub texinfo_remove_from_option_list($$)
{
my $var = shift;
my $values_array_ref = shift;
if (not $options_as_lists{$var}) {
return 0;
}
foreach my $value (@$values_array_ref) {
@{$cmdline_options->{$var}}
= grep {$_ ne $value} @{$cmdline_options->{$var}};
}
return 1;
}
# This function can be used to get main program variables
# customization values.
# For conversion customization variables, converter methods
# should be used instead, the implementation usually used being
# from Texinfo::Convert::Converter.
# It is possible to set up an interface similar to those used in
# converters for the main program configuration with the
# Texinfo::MainConfig below, but it should not be accessed/used
# in user defined code (and is therefore undocumented).
sub texinfo_get_conf($)
{
my $var = shift;
confess("BUG: texinfo_get_conf: undef \$cmdline_options."
." Call GNUT_initialize_config")
if (!$cmdline_options);
if (exists($cmdline_options->{$var})) {
return $cmdline_options->{$var};
} elsif (exists($init_files_options->{$var})) {
return $init_files_options->{$var};
} elsif (exists($main_program_default_options->{$var})) {
return $main_program_default_options->{$var};
} else {
return undef;
}
}
# to dynamically add customization options from init files
sub texinfo_add_valid_customization_option($)
{
my $option = shift;
return Texinfo::Common::add_valid_customization_option($option);
}
########################################################################
# Output format API. Handled differently from customization option
# because a function from main program need to be called on formats, so
# there is a function called from the main program to get the format set
# in init files.
my $init_file_format;
sub texinfo_set_format_from_init_file($)
{
$init_file_format = shift;
}
sub GNUT_get_format_from_init_file()
{
return $init_file_format;
}
#####################################################################
# stages handlers API. Used in HTML only.
my @possible_stages = ('setup', 'structure', 'init', 'finish');
my $default_priority = 'default';
# TODO add another level with format? Not needed now as HTML is
# the only customizable format for now.
my $GNUT_stage_handlers;
sub _GNUT_initialize_stage_handlers()
{
$GNUT_stage_handlers = {};
foreach my $stage (@possible_stages) {
$GNUT_stage_handlers->{$stage} = {};
}
}
_GNUT_initialize_stage_handlers();
sub texinfo_register_handler($$;$)
{
my $stage = shift;
my $handler = shift;
my $priority = shift;
if (!$GNUT_stage_handlers->{$stage}) {
carp ("Unknown stage $stage\n");
return 0;
}
$priority = $default_priority if (!defined($priority));
push @{$GNUT_stage_handlers->{$stage}->{$priority}}, $handler;
return 1;
}
# called from the Converter
sub GNUT_get_stage_handlers()
{
return $GNUT_stage_handlers;
}
#####################################################################
# API used to override formatting. Used in HTML only.
my $GNUT_file_id_setting_references = {};
my $GNUT_formatting_references = {};
my $GNUT_formatting_special_element_body = {};
my $GNUT_commands_conversion = {};
my $GNUT_commands_open = {};
my $GNUT_types_conversion = {};
my $GNUT_types_open = {};
my $GNUT_no_arg_commands_formatting_strings = {};
my $GNUT_style_commands_formatting_info = {};
my $GNUT_accent_command_formatting_info = {};
my $GNUT_types_formatting_info = {};
my $GNUT_direction_string_info = {};
my $GNUT_special_element_info = {};
# called from init files
sub texinfo_register_file_id_setting_function($$)
{
my $thing = shift;
my $handler = shift;
$GNUT_file_id_setting_references->{$thing} = $handler;
}
# called from the Converter
sub GNUT_get_file_id_setting_references()
{
return $GNUT_file_id_setting_references;
}
# called from init files
sub texinfo_register_formatting_function($$)
{
my $thing = shift;
my $handler = shift;
$GNUT_formatting_references->{$thing} = $handler;
}
# called from the Converter
sub GNUT_get_formatting_references()
{
return $GNUT_formatting_references;
}
# called from init files
sub texinfo_register_command_formatting($$)
{
my $command = shift;
my $reference = shift;
$GNUT_commands_conversion->{$command} = $reference;
}
# called from the Converter
sub GNUT_get_commands_conversion()
{
return $GNUT_commands_conversion;
}
# called from init files
sub texinfo_register_command_opening($$)
{
my $command = shift;
my $reference = shift;
$GNUT_commands_open->{$command} = $reference;
}
# called from the Converter
sub GNUT_get_commands_open()
{
return $GNUT_commands_open;
}
# called from init files
sub texinfo_register_type_formatting($$)
{
my $command = shift;
my $reference = shift;
$GNUT_types_conversion->{$command} = $reference;
}
# called from the Converter
sub GNUT_get_types_conversion()
{
return $GNUT_types_conversion;
}
# called from init files
sub texinfo_register_type_opening($$)
{
my $type = shift;
my $reference = shift;
$GNUT_types_open->{$type} = $reference;
}
# called from the Converter
sub GNUT_get_types_open()
{
return $GNUT_types_open;
}
# called from init files
sub texinfo_register_formatting_special_element_body($$)
{
my $special_element_variety = shift;
my $handler = shift;
$GNUT_formatting_special_element_body->{$special_element_variety} = $handler;
}
# called from the Converter
sub GNUT_get_formatting_special_element_body_references()
{
return $GNUT_formatting_special_element_body;
}
my $default_formatting_context = 'normal';
my @all_possible_formatting_context = ($default_formatting_context,
'preformatted', 'string', 'css_string');
sub _GNUT_initialize_no_arg_commands_formatting_strings()
{
$GNUT_no_arg_commands_formatting_strings = {};
foreach my $possible_formatting_context (@all_possible_formatting_context) {
$GNUT_no_arg_commands_formatting_strings->{$possible_formatting_context} = {};
}
}
_GNUT_initialize_no_arg_commands_formatting_strings();
sub _GNUT_initialize_style_commands_formatting_info()
{
$GNUT_style_commands_formatting_info = {};
foreach my $possible_formatting_context (@all_possible_formatting_context) {
$GNUT_style_commands_formatting_info->{$possible_formatting_context} = {};
}
}
_GNUT_initialize_style_commands_formatting_info();
my @all_special_element_info_types = ('class', 'direction', 'heading', 'order',
'file_string', 'target');
sub _GNUT_initialize_special_element_info()
{
$GNUT_special_element_info = {};
foreach my $possible_type (@all_special_element_info_types) {
$GNUT_special_element_info->{$possible_type} = {};
}
}
_GNUT_initialize_special_element_info();
# $translated_converted_string is supposed to be already formatted.
# It may also be relevant to be able to pass a 'tree'
# directly (it is actually handled by the converter code).
sub texinfo_register_no_arg_command_formatting($$;$$$$)
{
my $command = shift;
my $context = shift;
my $text = shift;
# html element
my $element = shift;
my $translated_converted_string = shift;
my $translated_to_convert_string = shift;
if (!defined($context)) {
$context = $default_formatting_context;
} elsif (not defined($GNUT_no_arg_commands_formatting_strings->{$context})) {
_GNUT_document_warn(sprintf(__("%s: unknown formatting context %s\n"),
'texinfo_register_no_arg_command_formatting', $context));
return 0;
}
my $specification = {};
if (defined($text)) {
$specification->{'text'} = $text;
}
if (defined($element)) {
$specification->{'element'} = $element;
}
if (defined($translated_converted_string)) {
$specification->{'translated_converted'} = $translated_converted_string;
# NOTE unset 'text'? A priori not needed, it will be overwritten
}
if (defined($translated_to_convert_string)) {
# only need to register in normal context, as the Texinfo code
# will be converted in the appropriate context.
if ($context ne $default_formatting_context) {
return 0;
}
$specification->{'translated_to_convert'} = $translated_to_convert_string;
}
$GNUT_no_arg_commands_formatting_strings->{$context}->{$command} = $specification;
return 1;
}
sub GNUT_get_no_arg_command_formatting($;$)
{
my $command = shift;
my $context = shift;
if (!defined($context)) {
$context = $default_formatting_context;
} elsif (not defined($GNUT_style_commands_formatting_info->{$context})) {
_GNUT_document_warn(sprintf(__("%s: unknown formatting context %s\n"),
'GNUT_get_no_arg_command_formatting', $context));
return undef;
}
if (exists($GNUT_no_arg_commands_formatting_strings->{$context})
and exists($GNUT_no_arg_commands_formatting_strings->{$context}->{$command})) {
return $GNUT_no_arg_commands_formatting_strings->{$context}->{$command};
}
return undef;
}
# called from init files
sub texinfo_register_style_command_formatting($$;$$)
{
my $command = shift;
my $html_element = shift;
my $in_quotes = shift;
my $context = shift;
if (!defined($context)) {
$context = $default_formatting_context;
} elsif (not defined($GNUT_style_commands_formatting_info->{$context})) {
_GNUT_document_warn(sprintf(__("%s: unknown formatting context %s\n"),
'texinfo_register_style_command_formatting', $context));
return 0;
}
my $specification = {};
if ($in_quotes) {
$specification->{'quotes'} = $in_quotes;
}
if (defined($html_element)) {
$specification->{'element'} = $html_element;
}
$GNUT_style_commands_formatting_info->{$context}->{$command} = $specification;
return 1;
}
sub GNUT_get_style_command_formatting($;$)
{
my $command = shift;
my $context = shift;
if (!defined($context)) {
$context = $default_formatting_context;
} elsif (not defined($GNUT_style_commands_formatting_info->{$context})) {
_GNUT_document_warn(sprintf(__("%s: unknown formatting context %s\n"),
'GNUT_get_style_command_formatting', $context));
return undef;
}
if (exists($GNUT_style_commands_formatting_info->{$context})
and exists($GNUT_style_commands_formatting_info->{$context}->{$command})) {
return $GNUT_style_commands_formatting_info->{$context}->{$command};
}
return undef;
}
# called from init files
sub texinfo_register_accent_command_formatting($$$)
{
my $command = shift;
my $accent_command_entity = shift;
my $accent_command_text_with_entities = shift;
$GNUT_accent_command_formatting_info->{$command}
= [$accent_command_entity, $accent_command_text_with_entities];
return 1;
}
# called from the Converter
sub GNUT_get_accent_command_formatting($)
{
my $command = shift;
if (exists($GNUT_accent_command_formatting_info->{$command})) {
return @{$GNUT_accent_command_formatting_info->{$command}};
}
return (undef, undef);
}
# need to give both arguments
# FIXME is it ok, or should there be two functions?
sub texinfo_register_type_format_info($$$)
{
my $type = shift;
my $code_type = shift;
my $pre_class_type = shift;
$GNUT_types_formatting_info->{$type}
= {'code' => $code_type, 'pre_class' => $pre_class_type};
}
sub GNUT_get_types_formatting_info()
{
# NOTE a deep copy could also be done if needed
return { %$GNUT_types_formatting_info };
}
# no check on type and direction, but only the ones known in the HTML
# converted will be used
sub texinfo_register_direction_string_info($$;$$$)
{
my $direction = shift;
my $type = shift;
my $converted_string = shift;
my $string_to_convert = shift;
my $context = shift;
$context = 'normal' if (!defined($context));
$GNUT_direction_string_info->{$type} = {}
if (not exists($GNUT_direction_string_info->{$type}));
$GNUT_direction_string_info->{$type}->{$direction} = {}
if (not exists($GNUT_direction_string_info->{$type}->{$direction}));
$GNUT_direction_string_info->{$type}->{$direction}->{'to_convert'}
= $string_to_convert;
if (defined($converted_string)) {
$GNUT_direction_string_info->{$type}->{$direction}->{'converted'} = {}
if (!defined($GNUT_direction_string_info->{$type}->{$direction}->{'converted'}));
$GNUT_direction_string_info->{$type}->{$direction}->{'converted'}->{$context}
= $converted_string;
}
}
sub GNUT_get_direction_string_info()
{
return { %$GNUT_direction_string_info };
}
sub texinfo_register_special_element_info($$$)
{
my $type = shift;
my $variety = shift;
my $thing = shift;
if (not defined($GNUT_special_element_info->{$type})) {
_GNUT_document_warn(
sprintf(__("%s: unknown special element information type %s\n"),
'texinfo_register_special_element_info', $type));
return 0;
}
$GNUT_special_element_info->{$type}->{$variety} = {}
if (not exists($GNUT_special_element_info->{$type}->{$variety}));
$GNUT_special_element_info->{$type}->{$variety} = $thing;
return 1;
}
sub GNUT_get_special_element_info()
{
return { %$GNUT_special_element_info };
}
# Not needed from the main program, as the init files should affect all
# the manuals, but needed for tests, to have isolated tests.
sub GNUT_reinitialize_init_files()
{
@init_file_loading_messages = ();
foreach my $reference ($init_files_options,
$GNUT_file_id_setting_references,
$GNUT_formatting_references, $GNUT_formatting_special_element_body,
$GNUT_commands_conversion, $GNUT_commands_open, $GNUT_types_conversion,
$GNUT_types_open, $GNUT_accent_command_formatting_info,
$GNUT_types_formatting_info, $GNUT_direction_string_info) {
$reference = {};
}
_GNUT_initialize_stage_handlers();
_GNUT_initialize_no_arg_commands_formatting_strings();
_GNUT_initialize_style_commands_formatting_info();
_GNUT_initialize_special_element_info();
}
#####################################################################
# the objective of this small package is to be in another
# scope than init files and setup blessed objects that can call
# get_conf() and set_conf() methods like a parser or a converter.
#
# For the main program, there is also the need to have
# access to configuration options in order to have get_conf()
# return the same as Texinfo::Config::texinfo_get_conf().
# This is obtained by calling new() without argument.
#
# In tests the situation is different as nothing from the
# Texinfo::Config space is used, it is assumed that the
# configuration is available as a hash reference key
# value. This is obtained by calling new() with an hash
# reference argument.
package Texinfo::MainConfig;
sub new(;$)
{
my $options = shift;
my $config;
if (defined($options)) {
# creates a new object based on input hash reference
$config = {'standalone' => 1, 'config' => {%$options}};
} else {
# use Texinfo::Config
$config = {'standalone' => 0, 'config' => {}};
}
bless $config;
return $config;
}
sub get_conf($$)
{
my $self = shift;
my $var = shift;
if ($self->{'standalone'}) {
if (defined($self->{'config'}->{$var})) {
return $self->{'config'}->{$var};
}
} else {
# as get_conf, but with self having precedence on
# main program defaults
if (exists($cmdline_options->{$var})) {
return $cmdline_options->{$var};
} elsif (exists($init_files_options->{$var})) {
return $init_files_options->{$var};
} elsif (exists($self->{'config'}->{$var})) {
return $self->{'config'}->{$var};
} elsif (exists($main_program_default_options->{$var})) {
return $main_program_default_options->{$var};
} else {
return undef;
}
}
}
sub set_conf($$$)
{
my $self = shift;
my $var = shift;
my $val = shift;
$self->{'config'}->{$var} = $val;
return 1;
}
1;