blob: 29e5f64d3a42b517f02dbfcc028de1cdcd9db207 [file]
# texindex.awk
#
# Copyright 2014-2026 Free Software Foundation, Inc.
#
# This file is part of GNU Texinfo.
#
# Texinfo 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.
#
# Texinfo 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/>.
########################################################################
#
# Library and helper functions
#
########################################################################
# Handle data file transitions
# This leads beginfile to be called at the beginning of every file
# and endfile at the end of every file.
# See Info node "(gawk)Join Function" for an explanation of how this function
# works.
FNR == 1 {
if (_filename_ != "")
endfile(_filename_)
_filename_ = FILENAME
beginfile(FILENAME)
}
END { endfile(_filename_) }
# Join an array of characters back into a string
# See Info node "(gawk)Join Function" for an explanation of how this function
# works.
function join(array, start, end, sep, result, i)
{
if (sep == "")
sep = " "
else if (sep == SUBSEP) # magic value
sep = ""
result = array[start]
for (i = start + 1; i <= end; i++)
result = result sep array[i]
return result
}
# quicksort --- C.A.R. Hoare's quick sort algorithm. See Wikipedia
# or almost any algorithms or computer science text
# Adapted from K&R-II, page 110
# Called as: quicksort(DATA, 1, COUNT, "string")
# The first argument is the list to be sorted.
# The fourth argument is the type of comparison to be used:
# * "string" - if data is an array of strings
# * "index" - if data is an array of index entries
#
function quicksort(data, left, right, compare, # parameters
i, last, use_index, lt) # locals
{
if (left >= right) # do nothing if array contains fewer
return # than two elements
# We set a Boolean (numeric) variable to indicate what kind of
# comparison to do, avoiding repeating the string
# comparison whose result won't change upon each iteration.
use_index = (compare == "index")
quicksort_swap(data, left, int((left + right) / 2))
last = left
for (i = left + 1; i <= right; i++) {
lt = (use_index \
? index_compare(data, i, left) \
: string_compare(data[i], data[left]) < 0)
if (lt)
quicksort_swap(data, ++last, i)
}
quicksort_swap(data, left, last)
quicksort(data, left, last - 1, compare)
quicksort(data, last + 1, right, compare)
}
# quicksort_swap --- quicksort helper function, could be inline
#
function quicksort_swap(data, i, j, temp)
{
temp = data[i]
data[i] = data[j]
data[j] = temp
}
# Clears out an array.
function del_array(a)
{
# Portable and faster than
# for (i in a)
# delete a[i]
split("", a)
}
# check_split_null - This function determines if (in the awk running
# this program) the built-in 'split()' function will split apart a string
# into its individual characters, or if we have to do it manually.
#
function check_split_null( n, a)
{
n = split("abcde", a, "")
# If this successed, the return value from split() will be the
# number of elements in the array, and it will be more than one.
Can_split_null = (n == 5)
}
# char_split - splits a string into separate characters, letting awk do
# the work if possible. If not, each character is extracted manually
# using a loop and substr().
#
function char_split(string, array, n, i)
{
if (Can_split_null)
return split(string, array, "")
# do it the hard way
del_array(array)
n = length(string)
for (i = 1; i <= n; i++)
array[i] = substr(string, i, 1)
return n
}
# The fatal() function prints a printf-formatted message to
# standard error and then exits badly.
# For maximal portability, it opens a pipeline to cat,
# redirected to standard error; not all systems have a /dev/stderr
# file, and not all versions of awk recognize that name
# internally. (Thus, we can't use 'print @dots{} > "/dev/stderr"'.)
#
function fatal(format, arg1, arg2, arg3, arg4, arg5,
arg6, arg7, arg8, arg9, arg10, cat)
{
cat = "cat 1>&2" # maximal portability
printf(format, arg1, arg2, arg3, arg4, arg5,
arg6, arg7, arg8, arg9, arg10) | cat
close(cat)
exit EXIT_FAILURE
}
# The following functions help identify what a character is; they are
# similar in nature to the various macros in the C <ctype.h> header
# file. Since most of them return a count, the return value could be used to
# compute which character from the set was seen; this turned out not to be
# necessary in this program but might be useful in some other context.
# By using index() with lists of letters, these functions will also work
# on EBCDIC systems, should that ever be necessary.
function isupper(c)
{
return index("ABCDEFGHIJKLMNOPQRSTUVWXYZ", c)
}
function islower(c)
{
return index("abcdefghijklmnopqrstuvwxyz", c)
}
function isalpha(c)
{
return islower(c) || isupper(c)
}
function isdigit(c)
{
return index("0123456789", c)
}
# texindex.awk has to handle input where the command character may be an
# @ or a \. When matching command strings in regular expressions, if
# the command character is a backslash, it must be doubled in order to be
# treated literally. The make_regexp() function handles this for us; a
# % in the text of the regexp stands for the command character and is
# replaced appropriately.
#
function make_regexp(regexp, # parameters
a, sep, n) # locals
{
n = split(regexp, a, "%")
if (Command_char == "\\")
sep = Command_char Command_char
else
sep = Command_char
return join(a, 1, n, sep)
}
# If the command character is a backslash, occurrences of backslash need
# to be doubled before the containing string can be used as a regexp.
# This function does that job
#
function escape(regexp, # parameters
a, n) # locals
{
if (Command_char != "\\")
return regexp
n = split(regexp, a, "\\")
if (n == 1)
return regexp
return join(a, 1, n, "\\\\")
}
function min(a, b)
{
return (a < b ? a : b)
}
########################################################################
#
# Main entry point, option processing and setup
#
########################################################################
function usage(exit_val)
{
printf(_"Usage: %s [OPTION]... FILE...\n", Invocation_name)
print _"Generate a sorted index for each TeX output FILE."
print _"Usually FILE... is specified as `foo.??' for a document `foo.texi'."
print ""
print _"Options:"
print _" -h, --help display this help and exit"
print _" --version display version information and exit"
print _" --combine-equal-keys combine entries based on sort key only"
print _" -- end option processing"
print ""
print _"Email bug reports to bug-texinfo@gnu.org,"
print _"general questions and discussion to help-texinfo@gnu.org."
print _"Texinfo home page: https://www.gnu.org/software/texinfo/"
exit exit_val
}
function version()
{
print "texindex (GNU texinfo)", Texindex_version
print ""
printf _"Copyright (C) %s Free Software Foundation, Inc.\n", "2026"
print _"License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>"
print _"This is free software: you are free to change and redistribute it."
print _"There is NO WARRANTY, to the extent permitted by law."
exit EXIT_SUCCESS
}
# We use the convention that global variable and array names start with a
# capital letter.
# We use multiple arrays to store different parts of the data. These are
# indexed by a string constructed from the data in the index entry.
# We need the following arrays:
#
# Numfields
# How many fields (entries) in this line: one, two, or three.
#
# Initials
# The initial for this line.
#
# Primary
# The primary index text. This is the real text, not the stripped
# value appearing in the sort key.
#
# Secondary
# The secondary index text, if present.
#
# Tertiary
# The tertiary index text, if present.
#
# Pagedata
# The page numbers on which identical entries occur.
#
# See_count
# The number of "see" entries for a given index line. Nothing
# prevents there being multiple such:
#
# @cindex toast @seeentry{toaster}
# @cindex toast @seeentry{jam}
#
# So we have to be prepared to handle such input. The See_count
# array counts the number of such texts as there may be.
#
# See
# The actual texts of the @seeentry value for the line's sort key.
# Each entry goes into See[key, 1], See[key, 2], etc., up to
# See_count[key].
#
# Seealso_count
# Seealso
# These serve the same purpose as See_count and See, but for
# @seealso entries.
# We additionally set the following for each file:
#
# Index - array of index entry lookup keys, indexed by integers starting at 1.
#
# Entries - number of entries in the Index array
#
# Allkeys - array indexed by index lookup key. Used to quickly detect duplicates.
#
# Subkeys - array containing parts of a sort key, indexed as Subkeys[KEY, I]
BEGIN {
TRUE = 1
FALSE = 0
EXIT_SUCCESS = 0
EXIT_FAILURE = 1
Texindex_version = "@VERSION@"
# --combine-equal-keys setting
CombineEqualKeys = FALSE
# Per GNU standards, we sometimes hardwire the string 'texindex'
# as the name of the program, and sometimes use the name
# by which the program was invoked. We'll call the latter
# 'Invocation_name'; it's supposed to be passed in from the
# shell wrapper.
if (! Invocation_name) {
# provide fallback in case it's not passed in.
Invocation_name = "texindex"
}
check_split_null()
# For gawk, we can arrange for various messages (e.g. in usage()
# and version()) to be translated. We do this by setting the text
# domain at startup. See Info node "(gawk)Internationalization"
# for more information.
#
# On non-GNU versions of awk, this is a harmless assignment, and
# the _"..." construct is a harmless concatenation of an unassigned
# variable _, i.e., the empty string, with the following string
# constant.
#
TEXTDOMAIN = "texinfo"
# Argument processing. Remove options and their arguments from
# 'ARGV' so that they're not treated as filenames.
for (i = 1; i < ARGC; i++) {
if (ARGV[i] == "-h" || ARGV[i] == "--help") {
usage(EXIT_SUCCESS)
} else if (ARGV[i] == "--version") {
version()
} else if (ARGV[i] == "-k" || ARGV[i] == "--keep") {
# do nothing, backwards compatibility
delete ARGV[i]
} else if (ARGV[i] == "--combine-equal-keys") {
CombineEqualKeys = TRUE
delete ARGV[i]
break
} else if (ARGV[i] == "--") {
delete ARGV[i]
break
} else if (ARGV[i] ~ /^--?.+/) {
fatal(_"%s: unrecognized option `%s'\n" \
"Try `%s --help' for more information.\n",
Invocation_name, ARGV[i], Invocation_name)
# fatal() will do `exit EXIT_FAILURE'
} else {
break
}
}
}
# Called at the start of each file. See "FNR == 1" block.
function beginfile(filename)
{
# Standard convention for name of output file.
Output_file = filename "s"
# Reinitialize these for each input file
del_array(Seen)
del_array(Index)
del_array(Allkeys)
del_array(Subkeys)
del_array(Initials)
del_array(Numfields)
del_array(Primary)
del_array(Secondary)
del_array(Tertiary)
del_array(See)
del_array(See_count)
del_array(Seealso)
del_array(Seealso_count)
del_array(Pagedata)
del_array(Printed)
Entries = 0
Do_initials = FALSE
Prev_initial = ""
# When 'beginfile' is called, the first record has already
# been read, so it's possible to perform the checks for a Texinfo
# index file.
Command_char = substr($0, 1, 1)
if ((Command_char != "\\" && Command_char != "@") \
|| substr($0, 2, 5) != "entry")
fatal(_"%s is not a Texinfo index file\n", filename)
# The characters that must be preceded by the command character
# inside the first key. This includes the command character itself.
Special_chars = "{}" Command_char
# These variables are regular expressions that match the
# corresponding TeX control sequences. They're initialized
# once for each input file, since the command character
# might be different between files.
Seeentry_re = make_regexp("%seeentry")
Seealso_re = make_regexp("%seealso")
Subentry_re = make_regexp(" *%subentry +")
}
# Called at the start of end of each file. (See "FNR == 1" block.)
# Sort the entries and write them out. Additionally, if we are printing
# the initial, handle that.
function endfile(filename, # parameters
i, prev_initial, initial) # locals
{
# sort the entries
quicksort(Index, 1, Entries, "index")
prev_initial = ""
for (i = 1; i <= Entries; i++) {
# deal with initial
initial = Initials[Index[i]]
if (initial != prev_initial) {
prev_initial = initial
print_initial(initial)
}
write_index_entry(Index[i])
}
close(Output_file)
}
########################################################################
#
# Record processing
#
########################################################################
# This block is run on every line of the input file. Build the data
# structures that will be used in sorting and printing.
{
# Remove duplicates, which can happen
if ($0 in Seen)
next
Seen[$0] = TRUE
# Remove leading \entry or @entry
# We use substr here to avoid possible hassles with leading
# backslashes in sub().
$0 = substr($0, 7)
initial = extract_initial($0)
# Pull out the data of interest from the multiple sets of braces.
numfields = field_split($0, fields, "{", "}", Command_char)
if (numfields < 3 || numfields > 5)
fatal(_"%s:%d: Bad entry; expected 3 to 5 fields, not %d\n",
FILENAME, FNR, numfields)
sortkey = fields[1]
pagenum = fields[2]
primary_text = fields[3]
secondary_text = (numfields > 3 ? fields[4] : "")
tertiary_text = (numfields > 4 ? fields[5] : "")
if (! CombineEqualKeys) {
# Combine entries if they have the same sort key
# and the same entry text.
Sep = "\001"
key = sortkey Sep primary_text Sep secondary_text \
Sep tertiary_text
} else {
# Combine entries if they have the same sort key
key = sortkey
}
# The Allkeys associative array lets us easily detect repeated
# index entries which should be combined. Note that it is more
# efficient to eliminate duplicates before sorting the array of
# index entries, as this makes the array smaller.
if (! (key in Allkeys)) {
# first time we've seen this full line
# Store the key in the Index array the first time it
# is seen; this array is sorted later on. Its indices
# are just incremented integers, stored in the global
# Entries variable.
Index[++Entries] = key
Allkeys[key] = 1
# Store data for this line in our global arrays
Initials[key] = initial
Numfields[key] = numfields - 2 # don't count sortkey, page number
Primary[key] = primary_text
if (secondary_text)
Secondary[key] = secondary_text
if (tertiary_text)
Tertiary[key] = tertiary_text
# Split out and store the subparts of the sort key, using
# "@subentry" as a separator.
n = split(sortkey, subparts, Subentry_re)
for (i = 1; i <= n; i++)
Subkeys[key, i] = subparts[i]
if (pagenum ~ Seeentry_re) {
See_count[key]++
See[key, See_count[key]] = pagenum
} else if (pagenum ~ Seealso_re) {
Seealso_count[key]++
Seealso[key, Seealso_count[key]] = pagenum
} else
Pagedata[key] = pagenum
} else {
# We've seen this key before:
# Add to see or see also, or else add to list of pages.
# In the latter case, make sure we've not seen this
# page number before. (Shouldn't happen based on the
# earlier removal of exact duplicates, but we could have
# an identical key with different formatting of actual text.
# For page numbers, we merely append the page number field
# from the input, preceded by a comma and space, unless
# that page number was already the last that's been stored.
# (We're assuming the page numbers don't jump around,
# which, in fact, they don't, so we don't need a more
# complex approach.) This also handles any page numbers
# that appear as roman numerals (from the so-called front
# matter), should there be such.
if (pagenum ~ Seeentry_re) {
See_count[key]++
See[key, See_count[key]] = pagenum
} else if (pagenum ~ Seealso_re) {
Seealso_count[key]++
Seealso[key, Seealso_count[key]] = pagenum
} else if (! (key in Pagedata)) {
Pagedata[key] = pagenum
} else if (Pagedata[key] != pagenum \
&& Pagedata[key] !~ escape(", " pagenum "$")) {
Pagedata[key] = Pagedata[key] ", " pagenum
}
}
# Determine if more than one initial occurs in the input.
# As soon as it's true, we don't need to do further
# checking on subsequent lines.
if (! Do_initials) {
if (Prev_initial == "")
Prev_initial = initial
else if (initial != Prev_initial)
Do_initials = TRUE
}
}
# extract_initial - extract initial character from key.
#
# The very first character of the sort key can be an open brace.
# If so, we extract the component of the sort key surrounded by balanced
# braces. We don't account for \{ or \} inside this component, as
# texinfo.tex isn't expected to produce such output.
#
# An example can be seen in what older versions of texinfo.tex generated
# if you needed to index a real backslash, namely an input line something
# like the following:
#
# \entry{{\tt \indexbackslash } (backslash)}{14}{\code {{\tt dots{}}}}
#
# Earlier versions of texindex took the first non-brace character as the
# initial, in this example \, and output it as \\; this was not, however,
# a control sequence recognized by the older versions of texinfo.tex.
function extract_initial(key, # parameters
initial, nextchar, i, l, kchars) # locals
{
l = char_split(key, kchars)
if (l >= 3 && kchars[2] == "{") {
bracecount = 1
i = 3
while (bracecount > 0 && i <= l) {
if (kchars[i] == "{")
bracecount++
else if (kchars[i] == "}")
bracecount--
i++
}
if (i > l)
fatal(_"%s:%d: Bad key %s in record\n", FILENAME, FNR, key)
initial = substr(key, 2, i - 2)
} else if (kchars[2] == Command_char) {
nextchar = kchars[3]
if (initial == Command_char && index("{}", nextchar) > 0)
initial = substr(key, 2, 3)
else {
initial = toupper(nextchar)
}
} else {
initial = toupper(kchars[2])
}
return initial
}
# field_split - break apart the record.
#
# RECORD looks something like: {POSIX awk}{5}{POSIX @command {awk}}.
#
# The first field may have instances of @{ and/or @} (or \{ and/or \})
# so the braces aren't necessarily exactly balanced.
#
# The field_split() function uses fairly straightforward ``count
# the delimiters'' code. The loop starts at two, since we know the first
# character is an open brace. The main things to handle are the command
# character and the final closing brace.
#
function field_split( \
record, fields, start, end, com_ch, # parameters
chars, numchars, out, delim_count, i, j, k) # locals
{
del_array(fields)
numchars = char_split(record, chars)
j = 1 # index into fields
k = 1 # index into out
delim_count = 1
for (i = 2; i <= numchars; i++) {
if (chars[i] == com_ch) {
# If the command character is doubled, we pass
# that on through, so that TeX will process
# it correctly.
#
# If the character following the command
# character is an open brace or close brace, we
# pull it in. Otherwise, the command character
# is left alone as part of the field.
if (chars[i+1] == Command_char) { # input was @@
out[k++] = chars[i+1]
out[k++] = chars[i+1]
i++
} else if (index(Special_chars, chars[i+1]) != 0) {
out[k++] = chars[i+1]
i++
} else
out[k++] = chars[i]
} else if (chars[i] == start) {
delim_count++
out[k++] = chars[i]
} else if (chars[i] == end) {
delim_count--
if (delim_count == 0) {
# Upon seeing the final closing brace, we
# put all the characters back together into
# a string using 'join()'. We then reset
# the 'out' array for the next time through.
# If the next character isn't an open brace,
# then the line is bad and we print a fatal
# error. Otherwise, we reset 'delim_count'
# to one.
fields[j++] = join(out, 1, k-1, SUBSEP)
del_array(out) # reset for next time through
k = 1
i++
if (i <= numchars && chars[i] != start)
fatal(_"%s:%d: Bad entry; expected %s at column %d\n",
FILENAME, FNR, start, i)
delim_count = 1
} else
out[k++] = chars[i]
} else
out[k++] = chars[i]
}
return j - 1 # num fields
}
# Print an initial. Precede special characters with the command
# character.
function print_initial(initial)
{
if (! Do_initials)
return
if (index(Special_chars, initial) != 0)
initial = Command_char initial
printf("%cinitial {%s}\n",
Command_char, initial) > Output_file
}
# The index_compare() function supplies the comparison for index entries.
#
# The index_compare() function has to take into account
# that we are comparing multilevel index entries. We can't just
# compare the full sort key, since the @subentry throws off
# the comparison; we want to compare based only on the key texts
# themselves.
#
# To that end, the comparison happens on two levels. At the higher
# level, we compare the subkeys; if the first subkeys are equal
# then we differentiate between them based on the second subkey.
# If, in turn, the second ones are equal, we differentiate based
# on the third one.
#
# By definition, an index entry with only one subkey sorts to be before
# an entry with two, and one with two comes before one with three.
#
# We make an effort here to call the comparison function only as
# much as necessary, since it's a relatively expensive operation.
#
function index_compare(data, l, r, # parameters
left, right, nfields, cmp) # locals
{
left = data[l]
right = data[r]
left_fields = Numfields[left]
right_fields = Numfields[right]
nfields = min(left_fields, right_fields)
# At least one field, always check the first subkey
cmp = string_compare(Subkeys[left, 1], Subkeys[right, 1])
if (cmp != 0)
return cmp < 0
# Compare the index text if the sort keys are identical
if (!CombineEqualKeys) {
cmp = string_compare(Primary[left], Primary[right])
if (cmp != 0)
return cmp < 0
}
# cmp == 0: one side has 1 field, other side has 1 to 3 fields
if (nfields == 1)
return left_fields < right_fields
# At least two fields, check second subkey
cmp = string_compare(Subkeys[left, 2], Subkeys[right, 2])
if (cmp != 0)
return cmp < 0
if (!CombineEqualKeys) {
cmp = string_compare(Secondary[left], Secondary[right])
if (cmp != 0)
return cmp < 0
}
# One side has 2 fields, the other has 2 to 3 fields
if (nfields == 2)
return left_fields < right_fields
# Three fields
cmp = string_compare(Subkeys[left, 3], Subkeys[right, 3])
if (cmp != 0)
return cmp < 0
if (!CombineEqualKeys) {
cmp = string_compare(Tertiary[left], Tertiary[right])
if (cmp != 0)
return cmp < 0
}
}
# Initialize Ordval array for use by string_compare.
# This array maps characters to numeric values for comparison, according
# to the following rules:
# * All symbols first.
# * Followed by digits.
# * Followed by letters. Lowercase precedes uppercase and both ``a'' and
# ``A'' precede anything starting with ``b'' or ``B'' (etc.).
#
# Most characters map to their ASCII code. We add 512 to the value of each
# of the digits; this causes them to come after all symbols.
# The letters are handled a little differently. We set things up so that
# lowercase letters come before uppercase ones, but both ``a'' and
# ``A'' come before ``b'', and so on. This then lets us use
# a simple subtraction in the comparison to determine if two letters are
# less than, equal to, or greater than each other. In any case, the mapping
# also ensures that letters come after digits.
# Ordval_ignorecase has the same ordering, except that lowercase and uppercase
# are not distinguished.
# (This code should also work for EBCDIC systems, although TeX does
# everything in ASCII, so it's not likely to make a difference.)
BEGIN {
for (i = 0; i < 256; i++) {
c = sprintf("%c", i)
Ordval[c] = i # map character to value
if (isdigit(c))
Ordval[c] += 512
}
# Set things up such that 'a' < 'A' < 'b' < 'B' < ...
i = Ordval["a"]
j = Ordval["z"]
newval = i + 512
for (; i <= j; i++) {
c = sprintf("%c", i)
if (islower(c)) {
Ordval[c] = newval++
Ordval[toupper(c)] = newval++
}
}
# Set up case-insensitive sorting order.
# In this order, multibyte UTF-8 sequences like é sort after
# ASCII letters.
for (i = 0; i < 256; i++) {
c = sprintf("%c", i)
Ordval_ignorecase[c] = i # map character to value
# Upper case comes before lower case in the ASCII order
# so value for upper case is already set.
if (islower(c))
Ordval_ignorecase[c] = Ordval_ignorecase[toupper(c)]
else if (isdigit(c))
Ordval_ignorecase[c] += 512
}
}
# Returns a three-way value a la the C strcmp() function: less than zero
# if the first string is less than the second, zero if they're equal,
# or greater than zero if the first string is greater than the second one.
#
function string_compare_internal(left, right, order, # parameters
len_l, len_r, len, chars_l, chars_r, # locals
order_l, order_r)
{
len_l = length(left)
len_r = length(right)
len = (len_l < len_r ? len_l : len_r)
char_split(left, chars_l)
char_split(right, chars_r)
for (i = 1; i <= len; i++) {
order_l = order[chars_l[i]];
order_r = order[chars_r[i]];
if (order_l != 0 && order_r != 0) {
cmp = order_l - order_r
} else {
# note that there is an incompatibility here between
# gawk and other implementations of awk (e.g. mawk).
# gawk may produce characters outside of the ASCII
# range which are not defined in the order array. in
# that case just compare the characters as strings.
if (chars_l[i] < chars_r[i])
cmp = -1
else if (chars_l[i] > chars_r[i])
cmp = 1
else
cmp = 0
}
if (cmp != 0)
return cmp
# equal, keep going
}
# equal so far, shorter one wins
if (len_l < len_r)
return -1
if (len_l > len_r)
return 1
return 0
}
function string_compare(left, right, # parameters
result) # locals
{
result = string_compare_internal(left, right, Ordval_ignorecase)
if (result == 0)
result = string_compare_internal(left, right, Ordval)
return result
}
# Print a single entry
function print_entry(key, entry_command, entry_text, # parameters
count, see_entries, i) # locals
{
if ((key, 1) in See) # at least one ``see''
print_see_entry(key, entry_command, entry_text,
See_count, See)
if (key in Pagedata) { # at least one page number
# When there exist both regular index entries for a topic and
# also a ``see also'' entry, we place the ``see also'' text
# after all the page numbers, so that there is only one printed
# index entry for the topic.
# This ends up being involved, since potentially there could be
# multiple ``see also'' entries (even though this is bad form).
if ((key, 1) in Seealso) {
# Although it's bad practice, there could be multiple
# ``see also'' entries for a given key. In that case,
# we must sort them before using them.
count = Seealso_count[key]
# Copy the entries to a separate array
for (i = 1; i <= count; i++)
see_entries[i] = Seealso[key, i]
# sort them
quicksort(see_entries, 1, count, "string")
# now add them to the page data
for (i = 1; i <= count; i++)
Pagedata[key] = Pagedata[key] ", " see_entries[i]
}
printf("%c%s{%s}{%s}\n",
Command_char, entry_command,
entry_text[key], Pagedata[key]) > Output_file
# Note that we only take note of having printed the
# key for lines with page numbers. Otherwise, a ``see''
# entry followed by a regular multilevel entry is not
# handled correctly.
Printed[key] = True # mark this key as printed
} else if ((key, 1) in Seealso) { # at least one ``see also''
# Only ``see also'' entry, print it
count = Seealso_count[key]
# Copy the entries to a separate array
for (i = 1; i <= count; i++)
see_entries[i] = Seealso[key, i]
# sort them
quicksort(see_entries, 1, count, "string")
printf("%c%s{%s}{",
Command_char, entry_command,
entry_text[key]) > Output_file
# now add them to the page data
for (i = 1; i <= count; i++) {
printf("%s", see_entries[i]) > Output_file
if (i != count)
printf(", ") > Output_file
}
printf("}\n") > Output_file
}
}
# Print a ``see'' entry. Potentially messy if there are more than
# one. (A good index won't have more than one, but nothing prevents there
# being multiple such entries, so we have to handle them.)
function print_see_entry(key, entry_command, entry_text, # parameters
count_array, see_text_array, # parameters
i, count, see_entries) # locals
{
count = count_array[key]
if (count == 1) { # the easy case
printf("%c%s{%s, %s}{}\n",
Command_char, entry_command,
entry_text[key], see_text_array[key, 1]) > Output_file
return
}
# Otherwise, we need to sort the entries and then print them
# Copy the entries to a separate array
for (i = 1; i <= count; i++)
see_entries[i] = see_text_array[key, i]
# sort them
quicksort(see_entries, 1, count, "string")
# now print them
for (i = 1; i <= count; i++)
printf("%c%s{%s, %s}{}\n",
Command_char, entry_command,
entry_text[key], see_entries[i]) > Output_file
}
function maybe_print_primary_entry(key, subkey)
{
# Print primary entry if necessary
# The subparts represent the key for those entries;
# each will have an index in Printed if
# we already printed such an entry.
if (CombineEqualKeys)
subkey = Subkeys[key, 1]
else
subkey = Subkeys[key, 1] Sep Primary[key] Sep Sep
if (! (subkey in Printed)) {
printf("%centry{%s,}{}\n",
Command_char, Primary[key]) > Output_file
Printed[subkey] = True
}
}
# Print secondary entry if necessary
function maybe_print_secondary_entry(key, subkey)
{
# We have to check that the combination of primary
# and secondary subkeys have been printed and
# use that combination as the index into Printed.
subkey = (Subkeys[key, 1] Command_char "subentry " Subkeys[key, 2])
if (!CombineEqualKeys)
subkey = subkey Sep Primary[key] Sep Secondary[key] Sep
if (! (subkey in Printed)) {
printf("%csecondary{%s,}{}\n",
Command_char, Secondary[key]) > Output_file
Printed[subkey] = True
}
}
# Print index entry with key KEY
#
# In some cases, print primary and/or secondary entries first.
# Consider the three-level case, for an entry like:
#
# @cindex coffee makers @subentry electric @subentry blue
#
# There may not have been separate preceding entries like ‘@cindex coffee
# makers’ or ‘@cindex coffee makers @subentry electric’. Thus, we have to
# generate the preceding ‘@entry’ and ‘@secondary’ lines before generating
# the final ‘@tertiary’ line.
#
function write_index_entry(key)
{
if (Numfields[key] == 1) {
print_entry(key, "entry", Primary)
} else if (Numfields[key] == 2) {
maybe_print_primary_entry(key)
print_entry(key, "secondary", Secondary)
} else if (Numfields[key] == 3) {
maybe_print_primary_entry(key)
maybe_print_secondary_entry(key)
print_entry(key, "tertiary", Tertiary)
}
}