blob: 578cae617c33634db23671da3c030ab755caded2 [file]
# Indices.pm: merge and sort indices. Functions without XS override.
#
# Copyright 2010-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>
#
# Most methods in that module are called from documents or
# converters methods that cache the results. Therefore, the relevant
# interface is, in the general case, with the document or converter
# when possible.
#
# ALTIMP C/main/manipulate_indices.c
package Texinfo::Indices;
use 5.006;
# See comment at start of HTML.pm
use if $] >= 5.012, feature => 'unicode_strings';
# Cannot do that because of sort_indices_by_letter, probably for uc().
# stop \s from matching non-ASCII spaces, etc. \p{...} can still be
# used to match Unicode character classes.
#use if $] >= 5.014, re => '/a';
use strict;
use warnings;
# Can be used to check that there is no incorrect autovivfication
#no autovivification qw(fetch delete exists store strict);
use Carp qw(cluck confess);
use Unicode::Normalize;
use Unicode::Collate;
use Texinfo::XSLoader;
use Texinfo::TreeElement;
use Texinfo::Common;
use Texinfo::Translations;
use Texinfo::Convert::Text;
our $VERSION = '7.3dev';
# In general, Texinfo::Document depends on this here module, but there
# is also a dependence to Texinfo::Document through the call to
# Texinfo::Document::indices_sort_strings call, hence a circular dependency.
# It is more natural to have Texinfo::Document import the here module,
# so we do not load Texinfo::Document here. If we did, there should not be any
# issue, though, as the modules do not setup data such that their order of
# loading is not important.
#use Texinfo::Document;
# The methods that are usefully called directly (outside of tests) are:
# index_entry_element_sort_string
# setup_index_entry_keys_formatting
# index_entry_first_letter_text_or_command
BEGIN {
my $shared_library_name = "IndicesXS";
if (!Texinfo::XSLoader::XS_convert_enabled()) {
undef $shared_library_name;
}
Texinfo::XSLoader::init (
"Texinfo::Indices",
"Texinfo::IndicesNonXS",
$shared_library_name,
undef,
['texinfo', 'texinfoxs', 'texinfo-convert', 'texinfo-convertxs'],
);
}
sub _sort_key($$) {
my ($a, $b) = @_;
my ($a_value, $a_alpha) = @$a;
my ($b_value, $b_alpha) = @$b;
if ($a_alpha == $b_alpha) {
return ($a_value cmp $b_value);
}
return $a_alpha <=> $b_alpha;
}
sub _sort_index_entries($$) {
my ($key1, $key2) = @_;
my $key_index = 0;
# the keys array corresponds to the main entry and subentries
foreach my $key1_str (@{$key1->{'keys'}}) {
my $res = _sort_key($key1_str,
$key2->{'keys'}->[$key_index]);
if ($res != 0) {
return $res;
}
$key_index++;
if (scalar(@{$key2->{'keys'}}) <= $key_index) {
last;
}
}
my $res = (scalar(@{$key1->{'keys'}}) <=> scalar(@{$key2->{'keys'}}));
if ($res == 0) {
$res = ($key1->{'number'} <=> $key2->{'number'});
}
# This may happen if 2 indices are merged as the number is per
# index name.
if ($res == 0) {
$res = ($key1->{'index_name'} cmp $key2->{'index_name'});
}
return $res;
}
# This is a stub for the Unicode::Collate module. It is used if
# USE_UNICODE_COLLATION=0. Using this fall-back will change index sorting,
# especially of punctuation characters and in non-English manuals.
#
# This fall-back also allows checking the performance impact of
# Unicode::Collate (last checked as about a 5% increase in runtime for
# typical Info output).
package Texinfo::CollateStub;
sub new($%) {
my $class = shift;
my %options = @_;
my $self = {};
bless $self, $class;
return $self;
}
# Simply return a copy of the string, in UTF-8.
# Note: this should return an encoded string, because Unicode::Collate
# getSortKey returns a byte string and if the getSortKey function
# is called from C code it may not handle any "wide characters" in
# the sort key.
# The ordering will be the Unicode code point order because the UTF-8
# sequences sort in code point order.
sub getSortKey($$) {
my ($self, $string) = @_;
utf8::encode($string);
return $string;
}
package Texinfo::Indices;
sub _remove_def_types($) {
my $element = shift;
delete $element->{'type'};
if (exists($element->{'contents'})
and exists($element->{'contents'}->[0]->{'type'})
and $element->{'contents'}->[0]->{'type'} eq 'bracketed_arg') {
# use brace_arg instead of bracketed_arg to avoid specific def
# type for the conversion of the index entry, but still have
# a type that have the same memory layout as bracketed_arg for C
$element->{'contents'}->[0]->{'type'} = 'brace_arg';
}
}
sub get_index_content_info_element($;$) {
my ($element, $prefer_reference_element) = @_;
if (!exists($element->{'extra'})
or !exists($element->{'extra'}->{'def_command'})) {
# the copy is not strictly needed, but we want to obtain the same
# result as with C and the result is different with anchors
# in index entries.
my $copy = Texinfo::ManipulateTree::copy_element_tree(
$element->{'contents'}->[0]);
return $copy, undef, undef;
}
my ($name, $class);
if (exists($element->{'contents'}->[0]->{'contents'})) {
foreach my $arg (@{$element->{'contents'}->[0]->{'contents'}}) {
my $type = $arg->{'type'};
if ($type eq 'def_name') {
$name = $arg;
} elsif ($type eq 'def_class') {
$class = $arg;
} elsif ($type eq 'def_arg' or $type eq 'def_typearg'
or $type eq 'delimiter') {
last;
}
}
}
if (defined($name)) {
my $def_command = $element->{'extra'}->{'def_command'};
my $name_copy = Texinfo::ManipulateTree::copy_element_tree($name);
if (!exists($Texinfo::Commands::def_class_variable_commands{
$def_command})
and !exists($Texinfo::Commands::def_class_method_commands{
$def_command})) {
_remove_def_types($name_copy);
return $name_copy, undef, undef;
}
if (defined($class)) {
my $class_copy = Texinfo::ManipulateTree::copy_element_tree($class);
foreach my $element_copy ($class_copy, $name_copy) {
_remove_def_types($element_copy);
}
if ($prefer_reference_element) {
my $text_element;
my $index_entry_normalized = Texinfo::TreeElement::new({});
if (exists($Texinfo::Commands::def_class_method_commands{
$def_command})) {
$text_element = Texinfo::TreeElement::new({'text' => ' on '});
} elsif (exists($Texinfo::Commands::def_class_variable_commands{
$def_command})) {
$text_element = Texinfo::TreeElement::new({'text' => ' of '});
}
$index_entry_normalized->{'contents'}
= [$name_copy, $text_element, $class_copy];
return $index_entry_normalized, undef, undef;
} else {
return undef, $name_copy, $class_copy;
}
}
}
return undef, undef, undef;
}
# generate a Texinfo tree corresponding to a def command index entry.
# If $PREFER_REFRENCE_ELEMENT is set do not translate. If there is
# no translation, $CONVERTER and $DEBUG_LEVEL are not actually used.
sub _def_command_index_entry($;$$$) {
my ($main_entry_element, $prefer_reference_element, $converter,
$debug_level) = @_;
my ($name, $class);
if (exists($main_entry_element->{'contents'}->[0]->{'contents'})) {
foreach my $arg (@{$main_entry_element->{'contents'}->[0]->{'contents'}}) {
my $type = $arg->{'type'};
if ($type eq 'def_name') {
$name = $arg;
} elsif ($type eq 'def_class') {
$class = $arg;
} elsif ($type eq 'def_arg' or $type eq 'def_typearg'
or $type eq 'delimiter') {
last;
}
}
}
if (defined($name)) {
my $def_command = $main_entry_element->{'extra'}->{'def_command'};
my $name_copy = Texinfo::ManipulateTree::copy_element_tree($name);
if (!exists($Texinfo::Commands::def_class_variable_commands{
$def_command})
and !exists($Texinfo::Commands::def_class_method_commands{
$def_command})) {
_remove_def_types($name_copy);
return $name_copy;
}
if (defined($class)) {
my $class_copy = Texinfo::ManipulateTree::copy_element_tree($class);
foreach my $element_copy ($class_copy, $name_copy) {
_remove_def_types($element_copy);
}
if ($prefer_reference_element) {
my $text_element;
my $index_entry_normalized = Texinfo::TreeElement::new({});
if (exists($Texinfo::Commands::def_class_method_commands{
$def_command})) {
$text_element = Texinfo::TreeElement::new({'text' => ' on '});
} elsif (exists($Texinfo::Commands::def_class_variable_commands{
$def_command})) {
$text_element = Texinfo::TreeElement::new({'text' => ' of '});
}
$index_entry_normalized->{'contents'}
= [$name_copy, $text_element, $class_copy];
return $index_entry_normalized;
} else {
my $index_entry;
# Use the language information that was current when the command was
# used for getting the translation.
my $substrings = {'name' => $name_copy, 'class' => $class_copy};
if (defined($converter)) {
if (exists($Texinfo::Commands::def_class_method_commands{
$def_command})) {
# TRANSLATORS: association of a method or operation name with a class
# in descriptions of object-oriented programming methods or operations.
$index_entry = $converter->element_cdt('{name} on {class}',
$main_entry_element, $substrings);
} elsif (exists($Texinfo::Commands::def_class_variable_commands{
$def_command})) {
# TRANSLATORS: association of a variable or instance variable with
# a class in descriptions of object-oriented programming variables or
# instance variable.
$index_entry = $converter->element_cdt('{name} of {class}',
$main_entry_element, $substrings);
}
} else {
my $element_lang_translations
= Texinfo::Translations::new_element_language_translation(
$Texinfo::Translations::converters_translation_cache,
$main_entry_element);
if (exists($Texinfo::Commands::def_class_method_commands{
$def_command})) {
# TRANSLATORS: association of a method or operation name with a class
# in descriptions of object-oriented programming methods or operations.
$index_entry
= Texinfo::Translations::gdt('{name} on {class}',
$element_lang_translations, $substrings,
$debug_level);
} elsif (exists($Texinfo::Commands::def_class_variable_commands{
$def_command})) {
# TRANSLATORS: association of a variable or instance variable with
# a class in descriptions of object-oriented programming variables or
# instance variable.
$index_entry = Texinfo::Translations::gdt('{name} of {class}',
$element_lang_translations, $substrings,
$debug_level);
}
}
# prefer a type-less container rather than 'root_line' returned by gdt
delete $index_entry->{'type'};
return $index_entry;
}
}
}
return undef;
}
# ALTIMP C/main/manipulate_indices.c
# if $PREFER_REFERENCE_ELEMENT is set, prefer an untranslated element.
sub document_index_content_element($;$$) {
my ($element, $prefer_reference_element, $debug_level) = @_;
my ($index_element, $name_copy, $class_copy)
= get_index_content_info_element($element, $prefer_reference_element);
if (defined($index_element) or !defined($class_copy)) {
return $index_element;
}
# Use the language information that was current when the command was
# used for getting the translation.
my $substrings = {'name' => $name_copy, 'class' => $class_copy};
my $def_command = $element->{'extra'}->{'def_command'};
my $element_lang_translations
= Texinfo::Translations::new_element_language_translation(
$Texinfo::Translations::converters_translation_cache,
$element);
if (exists($Texinfo::Commands::def_class_method_commands{
$def_command})) {
# TRANSLATORS: association of a method or operation name with a class
# in descriptions of object-oriented programming methods or operations.
$index_element
= Texinfo::Translations::gdt('{name} on {class}',
$element_lang_translations, $substrings,
$debug_level);
} elsif (exists($Texinfo::Commands::def_class_variable_commands{
$def_command})) {
# TRANSLATORS: association of a variable or instance variable with
# a class in descriptions of object-oriented programming variables or
# instance variable.
$index_element = Texinfo::Translations::gdt('{name} of {class}',
$element_lang_translations, $substrings,
$debug_level);
}
# prefer a type-less container rather than 'root_line' returned by gdt
delete $index_element->{'type'};
return $index_element;
}
sub converter_index_content_element($$;$) {
my ($element, $converter, $prefer_reference_element) = @_;
my ($index_element, $name_copy, $class_copy)
= get_index_content_info_element($element, $prefer_reference_element);
if (defined($index_element) or !defined($class_copy)) {
return $index_element;
}
my $substrings = {'name' => $name_copy, 'class' => $class_copy};
my $def_command = $element->{'extra'}->{'def_command'};
if (exists($Texinfo::Commands::def_class_method_commands{
$def_command})) {
# TRANSLATORS: association of a method or operation name with a class
# in descriptions of object-oriented programming methods or operations.
$index_element = $converter->element_cdt('{name} on {class}',
$element, $substrings);
} elsif (exists($Texinfo::Commands::def_class_variable_commands{
$def_command})) {
# TRANSLATORS: association of a variable or instance variable with
# a class in descriptions of object-oriented programming variables or
# instance variable.
$index_element = $converter->element_cdt('{name} of {class}',
$element, $substrings);
}
# prefer a type-less container rather than 'root_line' returned by gdt
delete $index_element->{'type'};
return $index_element;
}
# 'Non-Ignorable' for 'variable' collation characters means that they are
# treated as normal characters. This allows to have spaces and punctuation
# marks sort before letters.
# http://www.unicode.org/reports/tr10/#Variable_Weighting
my %collate_options = ( 'variable' => 'Non-Ignorable' );
# called from C/main/call_perl_function.c
sub _setup_lang_collator($;$) {
my ($lang_sorting_locale, $collate_options_ref) = @_;
$collate_options_ref = \%collate_options if (!defined($collate_options_ref));
my $collator;
# Unicode::Collate::Locale is present in perl core since perl major
# version 5.14 released in 2011.
eval { require Unicode::Collate::Locale;
Unicode::Collate::Locale->import; };
my $unicode_collate_locale_loading_error = $@;
if ($unicode_collate_locale_loading_error eq '') {
$collator = Unicode::Collate::Locale->new(
'locale' => $lang_sorting_locale,
%collate_options);
}
return $collator;
}
sub _setup_collator($$) {
my ($use_unicode_collation, $lang_sorting_locale) = @_;
my $collator;
# The Unicode::Collate sorting changes based on the UCA version.
# To test the result with a specific version, the UCA_Version should be set,
# and, more importantly the table should correspond to that version.
# To test a specific table, in the tta directory, do
# wget -N http://www.unicode.org/Public/UCA/6.2.0/allkeys.txt
# mkdir -p Unicode/Collate/
# mv allkeys.txt Unicode/Collate/allkeys-6.2.0.txt
# The table argument leads to a very important slowdown, so the argument
# should only be used for checks.
# The test results seem to be consistent with 6.2.0, corresponding
# to the perl 5.18.0 Unicode::Collate
# to test for 6.2.0
#%collate_options = (%collate_options,
# 'UCA_Version' => 24,
# 'table' => 'allkeys-6.2.0.txt');
# To test files affected for UCA corresponding to perl 5.8.1
# wget -N http://www.unicode.org/Public/UCA/3.1.1/allkeys-3.1.1.txt
#%collate_options = (%collate_options,
# 'UCA_Version' => 9,
# 'table' => 'allkeys-3.1.1.txt');
if (!(defined($use_unicode_collation) and !$use_unicode_collation)) {
if (defined($lang_sorting_locale)) {
$collator = _setup_lang_collator($lang_sorting_locale,
\%collate_options);
}
if (!defined($collator)) {
$collator = Unicode::Collate->new(%collate_options);
}
} else {
# stub if Unicode::Collate not wanted.
$collator = Texinfo::CollateStub->new() if (!defined($collator));
}
return $collator;
}
# Considered as kind of internal for index sorting functions, therefore
# not documented, no XS since it should not be called directly, but
# through Texinfo::Document::indices_sort_strings that caches the
# result in the document, itself called possibly through sorting functions.
sub setup_index_entries_sort_strings($$$$;$) {
my ($document, $converter, $index_entries, $indices_information,
$prefer_reference_element) = @_;
return undef unless (defined($index_entries));
my $document_info;
if (defined($converter)) {
$document_info = $converter;
} else {
$document_info = $document;
}
my $debug;
if (defined($document_info)) {
$debug = $document_info->get_conf('DEBUG');
}
# convert index entries to sort string using unicode when possible
# independently of input and output encodings
my $convert_text_options = {};
$convert_text_options->{'enabled_encoding'} = 'utf-8';
# It could be possible to set INCLUDE_DIRECTORIES, but there is no
# point doing so, as it is only useful for @verbatiminclude, which
# cannot appear in index entries.
#$convert_text_options->{'INCLUDE_DIRECTORIES'}
# = $converter->get_conf('INCLUDE_DIRECTORIES');
my $indices_sort_strings = {};
foreach my $index_name (keys(%$index_entries)) {
my @index_entries_sort_strings;
foreach my $index_entry (@{$index_entries->{$index_name}}) {
my $entry_index_name = $index_entry->{'index_name'};
my $main_entry_element = $index_entry->{'entry_element'};
my $in_code = $indices_information->{$entry_index_name}->{'in_code'};
if ($in_code) {
Texinfo::Convert::Text::set_options_code($convert_text_options);
}
# gather entry and subentries sort strings
my @entry_subentries_sort_strings;
my $non_empty_entry_subentries = 0;
my $entry_sort_string
= index_entry_element_sort_string($document_info,
$index_entry, $main_entry_element,
$convert_text_options, $prefer_reference_element,
$converter, $debug);
if ($entry_sort_string !~ /\S/) {
my $entry_cmdname = $main_entry_element->{'cmdname'};
$entry_cmdname
= $main_entry_element->{'extra'}->{'original_def_cmdname'}
if (!defined($entry_cmdname));
Texinfo::Common::converter_or_document_line_warn($document,
$converter,
sprintf(__("empty index key in \@%s"),
$entry_cmdname),
$main_entry_element->{'source_info'});
push @entry_subentries_sort_strings, '';
} else {
push @entry_subentries_sort_strings, $entry_sort_string;
$non_empty_entry_subentries++;
}
my $subentry_nr = 0;
my @subentries;
Texinfo::Common::collect_subentries($main_entry_element,
\@subentries);
foreach my $subentry (@subentries) {
$subentry_nr++;
my $subentry_sort_string
= index_entry_element_sort_string($document_info,
$index_entry, $subentry, $convert_text_options,
undef, $converter, $debug);
if ($subentry_sort_string !~ /\S/) {
my $entry_cmdname = $main_entry_element->{'cmdname'};
$entry_cmdname
= $main_entry_element->{'extra'}->{'original_def_cmdname'}
if (!defined($entry_cmdname));
Texinfo::Common::converter_or_document_line_warn($document,
$converter,
sprintf(__("empty index sub entry %d key in \@%s"),
$subentry_nr, $entry_cmdname),
$main_entry_element->{'source_info'});
push @entry_subentries_sort_strings, '';
} else {
push @entry_subentries_sort_strings, $subentry_sort_string;
$non_empty_entry_subentries++;
}
}
if ($non_empty_entry_subentries > 0) {
# register index entry sort strings information based on gathered
# entry and subentries sort strings and main entry information
my @entry_subentries_alpha_strings;
foreach my $sort_string (@entry_subentries_sort_strings) {
my $alpha = 0;
if ($sort_string =~ /^[[:alpha:]]/) {
$alpha = 1;
}
push @entry_subentries_alpha_strings,
{'sort_string' => $sort_string, 'alpha' => $alpha};
}
push @index_entries_sort_strings, {'entry' => $index_entry,
'sort_strings' => \@entry_subentries_alpha_strings,
'number' => $index_entry->{'entry_number'},
'index_name' => $entry_index_name};
}
if ($in_code) {
Texinfo::Convert::Text::reset_options_code($convert_text_options);
}
}
$indices_sort_strings->{$index_name} = \@index_entries_sort_strings;
}
return $indices_sort_strings;
}
# Returns a hash reference associating the index entries with the strings
# that were used to sort them.
# Used in tests, but not documented, as it is unlikely for this function
# to be of any other use.
sub format_index_entries_sort_strings($) {
my $indices_sort_strings = shift;
my $index_entries_sort_strings = {};
return $index_entries_sort_strings unless (defined($indices_sort_strings));
foreach my $index_name (keys(%$indices_sort_strings)) {
foreach my $index_entry (@{$indices_sort_strings->{$index_name}}) {
$index_entries_sort_strings->{$index_entry->{'entry'}}
= join(', ', map {$_->{'sort_string'}}
@{$index_entry->{'sort_strings'}});
}
}
return $index_entries_sort_strings;
}
sub _setup_sortable_index_entries($$) {
my ($collator, $indices_sort_strings) = @_;
return undef unless (defined($indices_sort_strings));
my $index_sortable_index_entries = {};
foreach my $index_name (keys(%$indices_sort_strings)) {
my $sortable_index_entries = [];
foreach my $index_entry (@{$indices_sort_strings->{$index_name}}) {
my @keys_and_alpha;
foreach my $entry_subentries_sort_string_alpha
(@{$index_entry->{'sort_strings'}}) {
my $sort_string = $entry_subentries_sort_string_alpha->{'sort_string'};
# TODO we do not mix decoded and non-decoded strings anymore so
# utf8::upgrade is probably not needed anymore. To be safe,
# we can keep it until we only support perl > 5.12. (5.14.0 released
# in 2011).
# This avoids varying results depending on whether the string is
# represented internally in UTF-8. See 'the "Unicode bug"' in the
# "perlunicode" man page.
utf8::upgrade($sort_string);
# $sort_key is a byte string
my $sort_key = $collator->getSortKey($sort_string);
push @keys_and_alpha, [$sort_key,
$entry_subentries_sort_string_alpha->{'alpha'}];
}
my $sortable_entry = {'entry' => $index_entry->{'entry'},
'keys' => \@keys_and_alpha,
'entry_strings_alpha' => $index_entry->{'sort_strings'},
'number' => $index_entry->{'number'},
'index_name' => $index_entry->{'index_name'}};
push @{$sortable_index_entries}, $sortable_entry;
}
$index_sortable_index_entries->{$index_name} = $sortable_index_entries;
}
return $index_sortable_index_entries;
}
sub _setup_sort_sortable_strings_collator($$$) {
my ($indices_sort_strings, $use_unicode_collation,
$lang_sorting_locale) = @_;
my $collator = _setup_collator($use_unicode_collation, $lang_sorting_locale);
my $index_sortable_index_entries
= _setup_sortable_index_entries($collator, $indices_sort_strings);
return ($index_sortable_index_entries, $collator);
}
# Normally called through Texinfo::Document::sorted_indices_by_index only
sub sort_indices_by_index($;$$) {
my ($indices_sort_strings, $use_unicode_collation, $lang_sorting_locale) = @_;
my ($index_sortable_index_entries, $collator)
= _setup_sort_sortable_strings_collator($indices_sort_strings,
$use_unicode_collation,
$lang_sorting_locale);
if (!defined($index_sortable_index_entries)) {
return undef;
}
my $sorted_index_entries = {};
foreach my $index_name (keys(%$index_sortable_index_entries)) {
my $sortable_index_entries = $index_sortable_index_entries->{$index_name};
$sorted_index_entries->{$index_name} = [
map {$_->{'entry'}} sort {_sort_index_entries($a, $b)}
@{$sortable_index_entries}
];
}
return $sorted_index_entries;
}
# Return the first non empty text or textual @-command.
# NOTE quotes and dash are not handled especially and it is not known
# if the text was in code or not.
sub _idx_leading_text_or_command($$);
sub _idx_leading_text_or_command($$) {
my ($tree, $ignore_chars) = @_;
return (undef, undef) if (!exists($tree->{'contents'}));
foreach my $content (@{$tree->{'contents'}}) {
if (exists($content->{'text'})) {
if ($content->{'text'} =~ /\S/) {
my $result_text = $content->{'text'};
$result_text =~ s/^\s*//;
if (defined($ignore_chars)) {
$result_text =~ s/[$ignore_chars]//g;
$result_text =~ s/^\s*//;
next if ($result_text eq '');
}
return ($result_text, undef);
} else {
next;
}
}
if (exists($content->{'cmdname'})) {
my $cmdname = $content->{'cmdname'};
if (exists($Texinfo::Commands::formatted_nobrace_commands{$cmdname})) {
next if (defined($ignore_chars) and $cmdname eq '@'
and $ignore_chars =~ /\@/);
return (undef, $content);
} else {
my $brace_command_type = $Texinfo::Commands::brace_commands{$cmdname};
if (defined($brace_command_type)) {
if (exists($Texinfo::Commands::non_formatted_brace_commands{$cmdname})
or $cmdname eq 'footnote' or $cmdname eq 'dmn'
or $cmdname eq 'value'
or exists($Texinfo::Commands::in_index_commands{$cmdname})) {
next;
} elsif ($brace_command_type eq 'accent'
or $brace_command_type eq 'noarg'
or $cmdname eq 'U') {
return (undef, $content);
} elsif ($brace_command_type ne 'inline') {
if (exists($content->{'contents'})) {
return _idx_leading_text_or_command($content->{'contents'}->[0],
$ignore_chars);
}
} else {
if (exists($content->{'extra'})
and defined($content->{'extra'}->{'expand_index'})) {
return _idx_leading_text_or_command($content->{'contents'}
->[$content->{'extra'}->{'expand_index'}],
$ignore_chars);
}
}
} elsif (exists($Texinfo::Commands::formatted_line_commands{$cmdname})
and $cmdname ne 'page') {
return _idx_leading_text_or_command($content->{'contents'}->[0],
$ignore_chars);
}
}
} elsif (exists($content->{'contents'})) {
return _idx_leading_text_or_command($content, $ignore_chars);
}
}
return (undef, undef);
}
# Return the leading text or textual command that could be used
# for sorting.
sub index_entry_first_letter_text_or_command($;$$) {
my ($index_entry, $converter, $debug_level) = @_;
if (!defined($index_entry)) {
confess('index_entry_first_letter_text_or_command: undef index_entry');
}
my $index_entry_element = $index_entry->{'entry_element'};
if (exists($index_entry_element->{'extra'})
and defined($index_entry_element->{'extra'}->{'sortas'})) {
return ($index_entry_element->{'extra'}->{'sortas'}, undef);
} else {
my $entry_tree_element
= converter_index_content_element($index_entry_element, 0,
$converter);
my $ignore_chars;
if (exists($index_entry_element->{'extra'})
and defined($index_entry_element->{'extra'}
->{'index_ignore_chars'})) {
$ignore_chars = quotemeta($index_entry_element->{'extra'}
->{'index_ignore_chars'});
}
my $parsed_element;
if (!exists($entry_tree_element->{'contents'})) {
$parsed_element
= Texinfo::TreeElement::new({'contents' => [$entry_tree_element]});
} else {
$parsed_element = $entry_tree_element;
}
my ($text, $command) = _idx_leading_text_or_command($parsed_element,
$ignore_chars);
return ($text, $command);
}
}
# Normally called through Texinfo::Document::sorted_indices_by_letter only
sub sort_indices_by_letter($;$$) {
my ($indices_sort_strings, $use_unicode_collation, $lang_sorting_locale) = @_;
my ($index_sortable_index_entries, $collator)
= _setup_sort_sortable_strings_collator($indices_sort_strings,
$use_unicode_collation,
$lang_sorting_locale);
if (!defined($index_sortable_index_entries)) {
return undef;
}
my $sorted_index_entries = {};
foreach my $index_name (keys(%$index_sortable_index_entries)) {
my $sortable_index_entries = $index_sortable_index_entries->{$index_name};
my $index_letter_hash = {};
foreach my $sortable_entry (@{$sortable_index_entries}) {
my $main_entry_sort_string
= $sortable_entry->{'entry_strings_alpha'}->[0]->{'sort_string'};
# the following line leads to each accented letter being separate
# $letter = uc(substr($main_entry_sort_string, 0, 1));
my $letter_string = uc(substr($main_entry_sort_string, 0, 1));
# determine main letter by decomposing and removing diacritics
my $letter = Unicode::Normalize::NFKD($letter_string);
$letter =~ s/\p{NonspacingMark}//g;
# following code is less good, as the upper-casing may lead to
# two letters in case of the german Eszett that becomes SS. So
# it is better to upper-case first and remove diacritics after.
#my $normalized_string
# = Unicode::Normalize::NFKD(uc($main_entry_sort_string));
#$normalized_string =~ s/\p{NonspacingMark}//g;
#$letter = substr($normalized_string, 0, 1);
push @{$index_letter_hash->{$letter}}, $sortable_entry;
}
my @letter_keys;
foreach my $letter (keys %$index_letter_hash) {
my $sort_key = $collator->getSortKey($letter);
push @letter_keys, [$sort_key, $letter, $index_letter_hash->{$letter}];
}
my @sorted_letters = sort{$a->[0] cmp $b->[0]} @letter_keys;
foreach my $letter_and_entries (@sorted_letters) {
my $letter = $letter_and_entries->[1];
my @sorted_letter_entries
= map {$_->{'entry'}} sort {_sort_index_entries($a, $b)}
@{$letter_and_entries->[2]};
push @{$sorted_index_entries->{$index_name}},
{ 'letter' => $letter, 'entries' => \@sorted_letter_entries };
}
}
return $sorted_index_entries;
}
# Normally called through Texinfo::Document::merged_indices only
sub merge_indices($) {
my $indices_information = shift;
my $merged_index_entries;
foreach my $index_name (keys(%$indices_information)) {
my $index_info = $indices_information->{$index_name};
if ($index_info->{'index_entries'}) {
$merged_index_entries = {} if (! $merged_index_entries);
my $in_idx_name = $index_name;
if ($index_info->{'merged_in'}) {
my $ultimate_idx = Texinfo::Common::ultimate_index($indices_information,
$index_info);
$in_idx_name = $ultimate_idx->{'name'};
}
push @{$merged_index_entries->{$in_idx_name}},
@{$index_info->{'index_entries'}};
}
}
return $merged_index_entries;
}
# textual representation on indices themselves (not on the index entries)
# used in tests
sub print_indices_information($) {
my $indices_information = shift;
return undef if (!defined($indices_information));
my @sorted_indices = sort(keys(%$indices_information));
return undef if (!scalar(@sorted_indices));
my $result = '';
foreach my $index_name (@sorted_indices) {
my $index_info = $indices_information->{$index_name};
$result .= $index_name;
if ($index_info->{'in_code'}) {
$result .= " C";
}
if (defined($index_info->{'merged_in'})) {
$result .= " ->$index_info->{'merged_in'}"
}
$result .= "\n";
}
return $result;
}
1;
__END__
=head1 NAME
Texinfo::Indices - merging and sorting indices from Texinfo
=head1 SYNOPSIS
use Texinfo::Indices;
# $document is a parsed Texinfo::Document document.
my $indices_information = $document->indices_information();
my $merged_index_entries
= Texinfo::Indices::merge_indices($indices_information);
my $indices_sort_strings
= Texinfo::Document::indices_sort_strings($document, undef);
my $index_entries_sorted;
if ($sort_by_letter) {
$index_entries_sorted
= Texinfo::Indices::sort_indices_by_letter($indices_sort_strings);
} else {
$index_entries_sorted
= Texinfo::Indices::sort_indices_by_index($indices_sort_strings);
}
=head1 NOTES
The Texinfo Perl module main purpose is to be used in C<texi2any> to convert
Texinfo to other formats. There is no promise of API stability.
=head1 DESCRIPTION
Main functions merge and sort indices. These functions should generally be
called indirectly.
Other functions help presenting sorting strings especially generated to be
output or are helper functions related to indices or index entries.
=head1 METHODS
No method is exported.
C<merge_indices> may be used to merge indices. Document indices may be sorted
with C<sort_indices_by_index> or C<sort_indices_by_letter>. The sorting may
be influenced by the current language. (In the current implementation,
sorting keys formatted internally in a language-independent manner are used.)
Note that, in general, the functions used to merge or sort indices
should not be called directly, corresponding functions
in L<Texinfo::Document> already call the functions in this module, and,
in addition, cache the result with the document. Furthermore, it should
be even better to call converter functions, which call document functions.
=over
=item $merged_indices = merge_indices($indices_information)
X<C<merge_indices>>
Returns a structure holding all the index entries by index name
with all the entries of merged indices merged with those of the indice
merged into. The I<$indices_information> argument should be an hash reference
with indices information, it is described in details in
L<< C<Texinfo::Document::indices_information>|Texinfo::Document/$indices_information = $document->indices_information() >>.
The I<$merged_indices> returned is a hash reference whose
keys are the index names and values arrays of index entry structures
described in details in L<Texinfo::Document/index_entries>.
In general, this method should not be called directly, instead
L<< C<Texinfo::Document::merged_indices>|Texinfo::Document/$merged_indices = $document->merged_indices() >>
should be called on a document, which calls C<merge_indices> if needed and
associate the merged indices to the document.
=item $index_entries_sorted = sort_indices_by_index($document, $converter, $use_unicode_collation, $lang_sorting_locale)
=item $index_entries_sorted = sort_indices_by_letter($document, $converter, $use_unicode_collation, $lang_sorting_locale)
X<C<sort_indices_by_index>> X<C<sort_indices_by_letter>>
C<sort_indices_by_letter> sorts by index and letter, while
C<sort_indices_by_index> sort all entries of an index together.
In both cases, a hash reference with index names as keys I<$index_entries_sorted>
is returned.
Indices are obtained from I<$document>, and should have been merged
previously, in general by using
L<< C<Texinfo::Document::merged_indices>|Texinfo::Document/$merged_indices = $document->merged_indices() >>.
By default, indices are sorted according to the I<Unicode Collation Algorithm>
defined in the L<Unicode Technical Standard
#10|http://www.unicode.org/reports/tr10/>, without language-specific collation
tailoring. If I<$use_unicode_collation> is set to 0, the sorting will not use
the I<Unicode Collation Algorithm> and simply sort according to the codepoints.
The optional I<$lang_sorting_locale> language is used for linguistic
tailoring of the sorting, if possible.
When sorting by letter, an array reference of letter hash references is
associated with each index name. Each letter hash reference has two
keys, a I<letter> key with the letter, and an I<entries> key with an array
reference of sorted index entries beginning with the letter. The letter
is a character string suitable for sorting letters, but is not necessarily
the best to use for output.
When simply sorting, the array of the sorted index entries is associated
with the index name.
The optional argument I<$converter> is used for error reporting if present,
otherwise the I<$document> is used.
In general, those methods should not be called directly, instead
L<< C<Texinfo::Document::sorted_indices_by_index>|Texinfo::Document/$sorted_indices = sorted_indices_by_index($document, $converter, $use_unicode_collation, $lang_sorting_locale) >>
or L<< C<Texinfo::Document::sorted_indices_by_letter>|Texinfo::Document/$sorted_indices = sorted_indices_by_letter($document, $converter, $use_unicode_collation, $lang_sorting_locale) >>
should be called on a document. The C<Texinfo::Document> functions call
C<sort_indices_by_index> or C<sort_indices_by_letter> if needed and associate
the sorted indices to the document.
=back
Functions help setting up textual sorting strings, for instance for output.
Although they may be generated by calling the same functions, these sorting
strings are independent from the sorting keys used internally to sort indices.
=over
=item $sort_string = index_entry_element_sort_string($document_info, $main_entry, $index_entry_element, $options, $prefer_reference_element, $converter, $debug_level)
X<C<index_entry_element_sort_string>>
Return a string suitable as a sorting string for index entries, possibly
to be used in output.
I<$document_info> is solely used by C code to retrieve the document data.
I<$document_info> can be a converter based on L<Texinfo::Convert::Converter> or
a L<Texinfo::Document> document.
The tree element index entry processed is I<$index_entry_element>,
and can be a C<@subentry>. I<$main_entry> is the main index entry
that can be used to gather information.
The I<$options> are options used for Texinfo conversion to text for the
generation of the sorting string and, when called directly (and not through
index sorting functions), are generally obtained from
L<setup_index_entry_keys_formatting|/$option = setup_index_entry_keys_formatting($customization_information)>
early on, and reused in every call.
If I<$prefer_reference_element> is set, prefer an untranslated
element for the formatting as sorting string.
If I<$converter> is set use the converter function for translation.
This argument is ignored in XS, so should only be set when called
from a function with an XS override, such that the argument
is set only from functions used if XS is not being used at all.
=item $option = setup_index_entry_keys_formatting($customization_information)
X<C<setup_index_entry_keys_formatting>>
Return options relevant for index keys sorting for conversion of Texinfo
to output.
Should be called early, since it sets up language information corresponding to
the language current at the end of the preamble. (Note that commands appearing
in index entry whose output may depend on a language are rare.)
=back
Other functions.
=over
=item $entry_content_element = index_content_element($element, $prefer_reference_element, $converter, $debug_level)
FIXME incorrect, does not exist anymore, converter and document variants.
Return a Texinfo tree element corresponding to the content of the index
entry associated to I<$element>. If I<$prefer_reference_element> is set,
prefer an untranslated element. If the element is an index command like
C<@cindex> or an C<@ftable> C<@item>, the content element is the argument
of the command. If the element is a definition line, the index entry
element is based on the name and class. If the I<$converter> optional
argument is set, use a converter method for the translation.
=item ($text, $command) = index_entry_first_letter_text_or_command($index_entry, $converter, $debug_level)
Return the I<$index_entry> leading text I<$text> or textual command Texinfo
tree hash reference I<$command>. Here textual commands means accent
commands, brace commands without arguments used for character and glyph
insertion and C<@U>.
This method can in particular be used to format the leading letter
of an index entry using I<$command> instead of using the sort string letters
returned by C<sort_indices_by_letter>.
=back
=head1 SEE ALSO
L<Texinfo manual|http://www.gnu.org/s/texinfo/manual/texinfo/>,
L<Texinfo::Document>.
=head1 AUTHOR
Patrice Dumas, E<lt>bug-texinfo@gnu.orgE<gt>
=head1 COPYRIGHT AND LICENSE
Copyright 2010- Free Software Foundation, Inc. See the source file for
all copyright years.
This library is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or (at
your option) any later version.
=cut