#############################################################################
##
## This file is part of GAP, a system for computational discrete algebra.
## This file's authors include Frank Lübeck.
##
## Copyright of GAP belongs to its developers, whose names are too numerous
## to list here. Please refer to the COPYRIGHT file for details.
##
## SPDX-License-Identifier: GPL-2.0-or-later
##
## The files helpbase.g{d,i} contain the interface between GAP's online help
## and the actual help books.
##
if IsHPCGAP
then
HELP_REGION:=NewSpecialRegion(
"HELP_REGION");
else
HELP_REGION:=fail;
# dummy placeholder
fi;
#############################################################################
##
#F # # # # # internal utility functions dealing with strings # # # # # # #
##
#############################################################################
##
#F StringStreamInputTextFile( <filename> ) . . . . . . .
## content of file as string stream, all '\r' are removed
##
## This is useful for text files with text to display, because the files
## can come with UNIX or DOS/Win line breaks.
## If this turns out to be of general interest, it can be officially
## documented.
##
InstallGlobalFunction(StringStreamInputTextFile,
function(fname)
local s;
s := StringFile(fname);
if s = fail
then
return s;
fi;
RemoveCharacters(s,
"\r");
return InputTextString(s);
end);
#############################################################################
##
#F IsDocumentedWord( <word>[, false ] ) . . . . . . . check documentation for
#F <word> in a search string
##
## Returns 'true' if <word> appears as word in some search string of the help
## system. By default this is checked case sensitively. If the optional second
## argument 'false' is given, the check is case insensitive.
##
## This utility will first be used in some debug tools showing what is newly
## installed by loading a package. Can be documented if desired.
##
BindGlobal(
"IsDocumentedWord",
function( arg )
local inid, word, case, simple, cword, book, matches, a, match;
inid:= Union( CHARS_DIGITS, CHARS_UALPHA,
"_", CHARS_LALPHA );
word := arg[
1];
if Length( arg ) >
1 and arg[
2] = false
then
case:= LowercaseString;
else
case:= IdFunc;
fi;
simple:= SIMPLE_STRING( word );
cword:= case( word );
for book in HELP_KNOWN_BOOKS[
1]
do
matches:= HELP_GET_MATCHES( [ book ], simple, true );
for a in Concatenation( matches )
do
match:= case( _StripEscapeSequences( a[
1].entries[ a[
2] ][
1] ) );
if cword in SplitString( match,
"", Difference( match, inid ) )
then
return true;
fi;
od;
od;
return false;
end);
#############################################################################
##
## TRANSATL . . . . . . . . . . list of pairs of different spelling patterns
##
## One could add more patterns following the following rules:
## - Each element of `TRANSATL' should be a list of length two.
## - Do not use capital letters; instead, truncate the first letter
## of the word if might or might not be capitalised.
## - Usage of patterns where one of spelling variants is an initial
## substring of another is permitted.
## - Modification of these rules (or using the patterns where one of
## spelling variants is the trailing substring of another) may require
## changing algorithms used in `FindMultiSpelledHelpEntries' and
## `HELP_SEARCH_ALTERNATIVES'.
BindGlobal(
"TRANSATL", MakeImmutable(
[ [
"atalogue",
"atalog" ],
[
"olour",
"olor" ],
[
"entre",
"enter" ],
[
"isation",
"ization" ],
[
"ise",
"ize" ],
[
"abeling",
"abelling" ],
[
"olvable",
"oluble" ],
[
"yse",
"yze" ],
[
"roebner",
"robner"]] ) );
#############################################################################
##
## HELP_SEARCH_ALTERNATIVES
##
## This function is used by HELP_GET_MATCHES to check if the search topic
## might have different spellings, looking for patterns from `TRANSATL'.
##
## It returns a list of suggested spellings of a string, for example:
##
## gap> HELP_SEARCH_ALTERNATIVES("TriangulizeMat");
## [ "TrianguliseMat", "TriangulizeMat" ]
## gap> HELP_SEARCH_ALTERNATIVES("CentralizerSolvableGroup");
## [ "CentraliserSolubleGroup", "CentraliserSolvableGroup",
## "CentralizerSolubleGroup", "CentralizerSolvableGroup" ]
##
## This approach may suggest wrong spellings for topics containing the
## substring "Size" or "size", since it's not possible to detect whether
## "size" is a part of another word or a word itself (e.g. both spellings
## "emphasize" and "emphasise" may be used). However, this only creates
## a tiny and really neglectible overhead (try e.g. `??SizesCentralisers'
## or `??Centralizers, Normalizers and Intersections'); however it ensures
## that help searches may be successful even if they use inconsistent
## spelling. In practice, we expect that the majority of help searches
## will match no more than one pattern. One could use the utility function
## `FindMultiSpelledHelpEntries' below to see that the help system contains
## about a dozen of entries which contains two occurrences of some patterns,
## and none with three or more of them.
##
## In addition, it ensures that the search for system setters and testers
## such as e.g. ?SetIsMapping and ?HasIsMapping will return corresponding
## attributes and properties. e.g. IsMapping.
##
BindGlobal(
"HELP_SEARCH_ALTERNATIVES",
function( topic )
local positions, patterns, pattern, where, what, variant, pos,
newwhere, newwhat, i, chop,
begin, topics, shorttopic, r;
positions:=[];
patterns:=[];
# Loop through all spelling patterns to check if there are any matches.
# Record starting positions and data about matching patterns.
for pattern in TRANSATL
do
# for each pattern we record starting positions and data separately
# to deal with double matches where one variant is a subset of another
where := [];
what := [];
for variant in pattern
do
pos := POSITION_SUBSTRING( topic, variant,
0 );
while pos <> fail
do
Add( where, pos );
Add( what, rec( start := pos,
finish := pos+Length(variant)-
1,
variant := variant,
pattern := pattern ) );
pos := POSITION_SUBSTRING( topic, variant, pos+Length(variant) );
od;
od;
if Length(where) >
0 then # we have at least one match
# now check if we have a double match ( like in "catalogue" and "catalog" )
if Length( Set( where ) ) = Length( where )
then
# no double matches, just store the data (SortParallel will be applied later)
Append( positions, where );
Append( patterns, what );
else
# we have double match - create the new list, taking only the
# match with the longer substring
SortParallel( where, what );
newwhere:=[ where[
1] ];
newwhat:=[ what[
1] ];
for i in [
2..Length(where)]
do
if where[i]<>where[i-
1]
then
Add(newwhere,where[i]);
Add(newwhat,what[i]);
else
if Length( what[i].variant ) > Length( what[i-
1].variant )
then
newwhat[Length(newwhat)]:=what[i];
fi;
fi;
od;
Append( positions, newwhere );
Append( patterns, newwhat );
fi;
fi;
od;
if Length(positions) >
0 then # matches found
# sort data about matches accordingly to their positions in `topic'.
SortParallel( positions, patterns );
# Now chop the string 'topic' into a list of lists, each of them either
# a list of all variants from the respective spelling pattern or just
# a one-element list with the "glueing" string between two patterns or
# a pattern and the beginning or end of the string.
chop:=[];
begin:=
1;
for i in [
1..Length(positions)]
do
Add( chop, [ topic{[
begin..patterns[i].start-
1 ]} ] );
Add( chop, patterns[i].pattern );
begin := Minimum( patterns[i].finish, Length(topic) )+
1;
od;
if begin <= Length( topic )
then
Add( chop, [ topic{[
begin..Length(topic)]} ] );
fi;
# Take the cartesian product of 'chop' and form spelling suggestions
# as concatenations of its elements.
topics := List( Cartesian(chop), Concatenation );
else # no matches
topics := [ topic ];
fi;
r := [];
# This ensures that e.g. `?HasIsMapping` will show `IsMapping` even if only the
# latter is documented. It is guaranteed that the help system will send search
# terms in lowercase. The requirement of the search term to have the length at
# least 5 and do not have a space after "has" or "set" is essential: it prevents
# "set stabiliser", "hash", "sets", "SetX" etc. to be handled in the same way.
for topic in topics
do
if Length(topic) >
4 and topic{[
1..
3]} in [
"has" ,
"set" ] and topic[
4]<>
' ' then
shorttopic := topic{[
4..Length(topic)]};
Append( r, [ shorttopic,
Concatenation(
"has", shorttopic),
Concatenation(
"set", shorttopic) ] );
else
Add(r, topic );
fi;
od;
Sort( r );
return( r );
end);
#############################################################################
##
#F FindMultiSpelledHelpEntries() . . . . . . check documentation for entries
## which might have 2 or more different spellings
##
## This utility may be used in checks of the help system by GAP developers.
##
## `HELP_GET_MATCHES' uses `HELP_SEARCH_ALTERNATIVES' to look for other possible
## spellings, e.g. Normaliser/Normalizer, Center/Centre, Solvable/Soluble,
## Analyse/Analyze, Factorisation/Factorization etc.
##
## "FindMultiSpelledHelpEntries" reports help entries that contains more
## than one occurrence of spelling patterns from the `TRANSATL' list.
## It may falsely report entries containing the substring "Size" or "size",
## since it's not possible to detect whether "size" is a part of another
## word or a word itself (e.g. both spellings "emphasize" and "emphasise"
## may be used).
##
BindGlobal(
"FindMultiSpelledHelpEntries",
function( )
local report, pair, word, book, matches, a, match, patterns, i, j, w, pos, nr, hits;
report:=[];
for pair in TRANSATL
do
word := pair[
1];
for book in HELP_KNOWN_BOOKS[
1]
do
matches:= HELP_GET_MATCHES( [ book ], word, false );
for a in Concatenation( matches )
do
match:= _StripEscapeSequences( a[
1].entries[ a[
2] ][
1] );
patterns:=[];
for i in [
1..Length(TRANSATL)]
do
patterns[i]:=[];
for j in [
1..Length(TRANSATL[i])]
do
w:=TRANSATL[i][j];
nr:=
0;
pos := POSITION_SUBSTRING( match, w,
0 );
while pos <> fail
do
nr:=nr+
1;
pos := POSITION_SUBSTRING( match, w, pos+Length(w) );
od;
patterns[i][j]:=nr;
od;
od;
# we just check that there are two or more matches, but in principle
# we calculated all to distinguish between different cases: different
# patterns; different spellings of same pattern; same spelling of
# same pattern appears more than once.
hits := Sum(Flat(patterns));
if hits >=
1 then
AddSet( report, MakeImmutable([ hits, book, match ]) );
fi;
od;
od;
od;
return report;
end);
#############################################################################
##
#F MATCH_BEGIN( <a>, <b> )
##
## tries to match beginning of words, where words are separated by single
## spaces; return `true' or `false'.
##
## No form of normalization is applied to <a> or <b>, so this should be done
## before calling MATCH_BEGIN.
##
InstallGlobalFunction(MATCH_BEGIN,
function( a, b )
local p,q;
if Length(a)=
0 and Length(b)=
0 then
return true;
fi;
## if 0 = Length(b) or Length(a) < Length(b) then
if Length(a) < Length(b)
then
return false;
fi;
p:=Position(b,
' ');
if p=fail
then
return a{[
1..Length(b)]} = b;
else
q:=Position(a,
' ');
if q=fail
then
q:=Length(a)+
1;
fi;
# cope with blanks
return MATCH_BEGIN(a{[
1..q-
1]},b{[
1..p-
1]}) and
MATCH_BEGIN(a{[q+
1..Length(a)]},b{[p+
1..Length(b)]});
fi;
end);
# Slight variant: returns -1 on false and number of exact matching
# words on true (>=0). Can be used to rank some matches higher.
InstallGlobalFunction(MATCH_BEGIN_COUNT,
function( a, b )
local p, q, r;
if Length(a)=
0 and Length(b)=
0 then
return
0;
fi;
if Length(a) < Length(b)
then
return -
1;
fi;
p:=Position(b,
' ');
if p=fail
then
p:=Position(a,
' ');
if p<>fail
then
a:=a{[
1..p-
1]};
fi;
if Length(b)<=Length(a) and a{[
1..Length(b)]} = b
then
if Length(a)= Length(b)
then
return
1;
else
return
0;
fi;
else
return -
1;
fi;
else
q:=Position(a,
' ');
if q=fail
then
q:=Length(a)+
1;
fi;
# cope with blanks
if MATCH_BEGIN(a{[
1..q-
1]},b{[
1..p-
1]})
then
r := MATCH_BEGIN_COUNT(a{[q+
1..Length(a)]},b{[p+
1..Length(b)]});
if r >=
0 then
if p = q
then
return
1+r;
else
return
0;
fi;
else
return -
1;
fi;
else
return -
1;
fi;
fi;
end);
#############################################################################
##
#F FILLED_LINE( <left>, <right>, <fill> )
##
## return string starting with string <left>, a number of characters <fill>
## and ending with string <right> 6 characters before end of screen.
##
InstallGlobalFunction(FILLED_LINE,
function( l, r, f )
local w, n;
w := SizeScreen()[
1] -
8;
if w <
8 then
return
"";
fi;
if w-
7 < Length(l)
then
l := Concatenation( l{[
1..w-
7]},
"..." );
fi;
if w-
7 < Length(r)
then
r := Concatenation( r{[
1..w-
7]},
"..." );
fi;
if w-
7 < Length(l) + Length(r)
then
r := Concatenation( r{[
1..w-
7-Length(l)]},
"..." );
fi;
w := w - Length(l) - Length(r);
n := ShallowCopy(l);
Add( n,
' ' );
while 0 < w
do
Add( n, f );
w := w -
1;
od;
Add( n,
' ' );
Append( n, r );
return n;
end);
InstallGlobalFunction(SIMPLE_STRING,
function(str)
local trans;
# we simply list here in Position i how character i-1 should be translated
trans :=Concatenation(
"\000\>\<\c\004\005\006\007\b\t\n\013\014\r\016\017\020\021\022\023\024\025",
"\026\027\030\031\032\033\034\035\036\037 !\000 &\000 *+ -./",
"0123456789: <=>? abcd",
"efghijklmnopqrstuvwxyz[\000]^_\000abcdefghijklmnopqrstuvwxyz{ }~",
"\177\200\201\202",
"\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225",
"\226\227\230\231\232\233\234\235\236\237\240",
"\241\242\244\244\246\246\250\250\251\252\253\254\255\256\257\260\261\262",
"\264\264\265\266\270\270\271\272\276\276\276\276\277aaaaaa",
"aceeeeiiiidnooooo\327ouuuuypsaaaaaaaceeeeiiiidnooooo\367ouuuuypy"
);
CONV_STRING(str);
str := trans{List(str, INT_CHAR) +
1};
# we throw away zero characters (and so backslashes and quotes)
str := Filtered(str,x->x<>
'\000');
NormalizeWhitespace(str);
return str;
end);
#############################################################################
##
## Each book for GAP's help system has to be initialized by entries in
## HELP_KNOWN_BOOKS. These contain a short name (a single word), a long
## name and the directory of the documentation.
##
## For the main books of the GAP library this is included
## here, for packages these initializations are done by
## `LoadPackageDocumentation'.
##
## In the path for a help book there must be a file `manual.six'. It
## contains the indexing information used for the search of a topic in
## the GAP help. The format of the file is not prescribed. But if it is
## different from the current GAP library documentation format then the
## first line must be
##
## #SIXFORMAT myownformat
##
## Then a function HELP_BOOK_HANDLER.myownformat.ReadSix is used to read
## the rest of the file. (See HELP_BOOK_HANDLER below.)
##
# in first list: normalized names of books
# in second list: for each book a list
# [short name, long name,
# directory containing the manual.six file]
BindGlobal(
"HELP_KNOWN_BOOKS", [[],[]]);
if IsHPCGAP
then
LockAndMigrateObj(HELP_KNOWN_BOOKS,HELP_REGION);
fi;
# if book with normalized name is already installed, we overwrite, if dir
# is the same (so short and long can be changed)
# or if short corresponds to an installed "(not loaded)" version,
# else we raise an error
# dir can be given as string relative to GAP's home or as directory object
InstallGlobalFunction(HELP_ADD_BOOK,
function( short, long, dir )
local sortfun, str, hnb, pos;
# we sort books with main books first and packages alphabetically,
# (looks a bit lengthy)
sortfun :=
function(a, b)
local main, pa, pb;
main := [
"tutorial",
"reference",
"hpc-gap",
"development" ];
pa := Position(main, a);
pb := Position(main, b);
if pa <> fail
then
if pb = fail
then
return true;
else
return pa <= pb;
fi;
else
if pb <> fail
then
return false;
else
return a < b;
fi;
fi;
end;
str := SIMPLE_STRING(short);
hnb := HELP_KNOWN_BOOKS;
# check if we reinstall a known book (with possibly other names)
pos := First([
1..Length(hnb[
2])], i-> dir = hnb[
2][i][
3]);
if not (Position(hnb[
1], str) in [fail, pos])
then
Info(InfoWarning,
1,
"Overwriting already installed help book '",str,
"'.");
Unbind(HELP_BOOKS_INFO.(str));
pos := Position(hnb[
1], str);
fi;
if pos = fail
then
# Perhaps we want to replace a "(not loaded)" book by another one.
pos:= Position( hnb[
1], Concatenation( str,
" not loaded" ) );
if pos = fail
then
pos := Length(hnb[
1]) +
1;
else
Unbind(HELP_BOOKS_INFO.(hnb[
1][pos]));
fi;
elif IsBound(HELP_BOOKS_INFO.(hnb[
1][pos]))
then
# rename help book info if already loaded
HELP_BOOKS_INFO.(str) := HELP_BOOKS_INFO.(hnb[
1][pos]);
# adjust .bookname
HELP_BOOKS_INFO.(str).bookname := short;
Unbind(HELP_BOOKS_INFO.(hnb[
1][pos]));
fi;
hnb[
1][pos] := str;
hnb[
2][pos] := [short, long, dir];
SortParallel(hnb[
1], hnb[
2], sortfun);
end);
InstallGlobalFunction(HELP_REMOVE_BOOK,
function( short )
local str, pos;
str := SIMPLE_STRING(short);
pos := Position(HELP_KNOWN_BOOKS[
1], str);
if pos = fail
then
Error(
"Book with normalized name ", str,
" is not installed.");
else
Remove (HELP_KNOWN_BOOKS[
1], pos);
Remove (HELP_KNOWN_BOOKS[
2], pos);
Unbind (HELP_BOOKS_INFO.(str));
fi;
end);
#############################################################################
##
#V HELP_BOOK_HANDLER
##
## We use a record to store handler for different tasks with a help book.
## The handler for the current library books is called "default". A
## handler is a record with some functions as components, at least there
## must be:
##
## - ReadSix # reading a BOOK_INFO from a manual.six stream
## - ShowChapters # returns text or lines with chapter headers
## - ShowSections # same for section headers
## - SearchMatches # returns list of numbers referring to entries in
## # BOOK_INFO's .entries list
## - MatchPrevChap # number of match for "<<" (last in HELP_LAST.BOOK
## - MatchNextChap # number of match for ">>" and HELP_LAST.MATCH)
## - MatchPrev # number of match for "<"
## - MatchNext # number of match for ">"
## - HelpData # returns for given number of entry in .entries the
## # corresponding help data for a given format
## # (a special format is "ref" for cross references,
## # see HELP_BOOK_HANDLER.HelpDataRef below for
## # details)
## The `default' handler functions will be assigned helpdef.g, see there for
## more details on the interfaces of each of these functions.
##
BindGlobal(
"HELP_BOOK_HANDLER", rec(default:=rec()));
if IsHPCGAP
then
LockAndMigrateObj(HELP_BOOK_HANDLER,HELP_REGION);
fi;
#############################################################################
##
#V HELP_BOOKS_INFO . . . . . . . . . . . . . collected info about the books
##
## The record <HELP_BOOKS_INFO> contains for each loaded help book an
## entry describing the information found in the "manual.six" file.
## This information is stored in a record with at least the following
## components, which are used by this generic interface to the help system:
##
## bookname:
##
## The short name of the book, e.g. "ref", "matrix", "EDIM".
##
## entries:
##
## List of entries for the search, each entry must be a list. In the
## first position there must be a string which is shown for this match,
## in case several matches for a topic where found.
##
## formats: (not necessary ???)
##
## List of output formats available for this book (like ["text", "url", ..]),
## this must contain at least "text".
##
## The remaining positions in the .entries lists and/or further
## components in this help book record depend on the format of the
## documentation and the corresponding handler functions.
##
BindGlobal(
"HELP_BOOKS_INFO", rec());
if IsHPCGAP
then
LockAndMigrateObj(HELP_BOOKS_INFO,HELP_REGION);
fi;
#############################################################################
##
#F HELP_BOOK_INFO( <book> ) . . . . . . . . . . . . . get info about a book
##
## Returns the corresponding HELP_BOOKS_INFO entry or reads in the
## corresponding manual.six file, if not yet done.
##
## <book> must be a record, which is just returned, or the short name of a
## known book.
##
InstallGlobalFunction(HELP_BOOK_INFO,
function( book )
local pos, bnam, path, dirs, six, stream, line, handler;
# if this is already a record return it
if IsRecord(book)
then
return book;
fi;
book := LowercaseString(book);
pos := Position(HELP_KNOWN_BOOKS[
1], book);
if pos = fail
then
# try to match beginning
pos := Filtered(HELP_KNOWN_BOOKS[
1], bn-> MATCH_BEGIN(bn, book));
if Length(pos) =
0 then
# give up
return fail;
else
pos := Position(HELP_KNOWN_BOOKS[
1], pos[
1]);
fi;
fi;
# now we have the (short) name of the book
bnam := HELP_KNOWN_BOOKS[
1][pos];
if IsBound(HELP_BOOKS_INFO.(bnam))
then
# done
return HELP_BOOKS_INFO.(bnam);
fi;
# get the filename of the "manual.six" file
path := HELP_KNOWN_BOOKS[
2][pos][
3];
if IsDirectory(path)
then
dirs := [path];
else
dirs := DirectoriesLibrary( path );
fi;
six := Filename( dirs,
"manual.six" );
if six = fail
then
# give up
return fail;
fi;
# read the manual.six file
# read the first non-empty line to find out the handler for the corresponding
# manual format (no explicit format implies the "default" handler)
stream := InputTextFile(six);
line :=
"";
while Length(line) =
0 do
line := ReadLine(stream);
if line=fail
then
CloseStream(stream);
return fail;
fi;
line := NormalizedWhitespace(line);
od;
if Length(line)>
10 and line{[
1..
10]}=
"#SIXFORMAT" then
handler := line{[
12..Length(line)]};
NormalizeWhitespace(handler);
else
handler :=
"default";
RewindStream(stream);
fi;
# give up if handler functions are not (yet) loaded
if not IsBound(HELP_BOOK_HANDLER.(handler))
then
Print(
"\n#W WARNING: No handler for help book `",
HELP_KNOWN_BOOKS[
2][pos][
1],
"' available,\n#W removing this book.\n");
if handler =
"GapDocGAP" then
Print(
"#W HINT: Install and load the GAPDoc package, see\n",
"#W http://www.math.rwth-aachen.de/~Frank.Luebeck/GAPDoc\n");
fi;
HELP_KNOWN_BOOKS[
1][pos] := Concatenation(
"XXXX ", bnam,
": THROWN OUT");
HELP_KNOWN_BOOKS[
2][pos][
2] :=
"NOT AVAILABLE (no handler)";
return fail;
fi;
HELP_BOOKS_INFO.(bnam) := HELP_BOOK_HANDLER.(handler).ReadSix(stream);
# adjust some entries used on the interface level
HELP_BOOKS_INFO.(bnam).handler := handler;
HELP_BOOKS_INFO.(bnam).bookname := HELP_KNOWN_BOOKS[
2][pos][
1];
# done
return HELP_BOOKS_INFO.(bnam);
end);
#############################################################################
##
##
#F # # # # # # # # # # generic show functions # # # # # # # # # # # # # # #
##
#############################################################################
##
## The central function for the help system is, of course, `HELP' below.
## Depending on the search string it may trigger different actions. The
## functions for these actions are defined first. Many of them delegate the
## actual work to the handler functions for the available books.
##
#############################################################################
##
#F HELP_SHOW_BOOKS( ignored... ) . . . . . . . . . . . show available books
##
InstallGlobalFunction(HELP_SHOW_BOOKS,
function( arg )
local books;
books := [
" Table of currently available help books",
FILLED_LINE(
"short name for ? commands",
"Description",
'_')];
Append(books, List(HELP_KNOWN_BOOKS[
2], a-> FILLED_LINE(a[
1], a[
2],
' ')));
Pager(books);
return true;
end);
#############################################################################
##
#F HELP_SHOW_CHAPTERS( <book> ) . . . . . . . . . . . . . show all chapters
##
InstallGlobalFunction(HELP_SHOW_CHAPTERS,
function(book)
local info;
# delegate to handler
info := HELP_BOOK_INFO(book);
if info = fail
then
Print(
"#W Help: Book ", book, " not found.\n");
else
HELP_LAST.BOOK := book;
HELP_LAST.MATCH :=
1;
Pager(HELP_BOOK_HANDLER.(info.handler).ShowChapters(info));
fi;
return true;
end);
#############################################################################
##
#F HELP_SHOW_SECTIONS( <book> ) . . . . . . . . . . . . . show all sections
##
InstallGlobalFunction(HELP_SHOW_SECTIONS,
function(book)
local info;
# delegate to handler
info := HELP_BOOK_INFO(book);
if info = fail
then
Print(
"#W Help: Book ", book, " not found.\n");
else
HELP_LAST.BOOK := book;
HELP_LAST.MATCH :=
1;
Pager(HELP_BOOK_HANDLER.(info.handler).ShowSections(info));
fi;
return true;
end);
#############################################################################
##
#F HELP_PRINT_MATCH( <match> ) . . . . . . the core function which finally
## gets the data for displaying the help and displays it
##
## <match> is [book, entrynr]
##
InstallGlobalFunction(HELP_PRINT_MATCH,
function(match)
local book, entrynr, viewer, hv, pos, type, data;
book := HELP_BOOK_INFO(match[
1]);
entrynr := match[
2];
viewer:= UserPreference(
"HelpViewers");
if HELP_LAST.NEXT_VIEWER = false
then
hv := viewer;
else
pos := Position( viewer, HELP_LAST.VIEWER );
if pos = fail
then
hv := viewer;
else
hv := viewer{Concatenation([pos+
1..Length(viewer)],[
1..pos])};
fi;
HELP_LAST.NEXT_VIEWER := false;
fi;
for viewer in hv
do
# type of data we need now depends on help viewer
type := HELP_VIEWER_INFO.(viewer).type;
# get the data via appropriate handler
data := HELP_BOOK_HANDLER.(book.handler).HelpData(book, entrynr, type);
if data <> fail
then
# show the data
HELP_VIEWER_INFO.(viewer).show(data);
break;
fi;
HELP_LAST.VIEWER := viewer;
od;
HELP_LAST.BOOK := book;
HELP_LAST.MATCH := entrynr;
HELP_LAST.VIEWER := viewer;
return true;
end);
#############################################################################
##
#F HELP_SHOW_PREV_CHAPTER( <book> ) . . . . . . . . show chapter introduction
##
InstallGlobalFunction(HELP_SHOW_PREV_CHAPTER,
function( arg )
local info, match;
if HELP_LAST.BOOK =
0 then
Print(
"Help: no history so far.\n");
return;
fi;
info := HELP_BOOK_INFO(HELP_LAST.BOOK);
match := HELP_BOOK_HANDLER.(info.handler).MatchPrevChap(info,
HELP_LAST.MATCH);
if match[
2] = fail
then
Print(
"Help: no match found.\n");
else
HELP_PRINT_MATCH(match);
HELP_LAST.MATCH := match[
2];
fi;
end);
#############################################################################
##
#F HELP_SHOW_NEXT_CHAPTER( <book> ) . . . . . . . . . . . show next chapter
##
InstallGlobalFunction(HELP_SHOW_NEXT_CHAPTER,
function( arg )
local info, match;
if HELP_LAST.BOOK =
0 then
Print(
"Help: no history so far.\n");
return;
fi;
info := HELP_BOOK_INFO(HELP_LAST.BOOK);
match := HELP_BOOK_HANDLER.(info.handler).MatchNextChap(info,
HELP_LAST.MATCH);
if match[
2] = fail
then
Print(
"Help: no match found.\n");
else
HELP_PRINT_MATCH(match);
HELP_LAST.MATCH := match[
2];
fi;
end);
#############################################################################
##
#F HELP_SHOW_PREV( <book> ) . . . . . . . . . . . . . show previous section
##
InstallGlobalFunction(HELP_SHOW_PREV,
function( arg )
local info, match;
if HELP_LAST.BOOK =
0 then
Print(
"Help: no history so far.\n");
return;
fi;
info := HELP_BOOK_INFO(HELP_LAST.BOOK);
match := HELP_BOOK_HANDLER.(info.handler).MatchPrev(info,
HELP_LAST.MATCH);
if match[
2] = fail
then
Print(
"Help: no match found.\n");
else
HELP_PRINT_MATCH(match);
HELP_LAST.MATCH := match[
2];
fi;
end);
#############################################################################
##
#F HELP_SHOW_NEXT( <book> ) . . . . . . . . . . . . . . . show next section
##
InstallGlobalFunction(HELP_SHOW_NEXT,
function( arg )
local info, match;
if HELP_LAST.BOOK =
0 then
Print(
"Help: no history so far.\n");
return;
fi;
info := HELP_BOOK_INFO(HELP_LAST.BOOK);
match := HELP_BOOK_HANDLER.(info.handler).MatchNext(info,
HELP_LAST.MATCH);
if match[
2] = fail
then
Print(
"Help: no match found.\n");
else
HELP_PRINT_MATCH(match);
HELP_LAST.MATCH := match[
2];
fi;
end);
#############################################################################
##
#F HELP_SHOW_WELCOME( <book> ) . . . . . . . . . . . . show welcome message
##
InstallGlobalFunction(HELP_SHOW_WELCOME,
function( book )
local lines;
lines := [
" Welcome to GAP 4\n",
" Try '?tutorial: The Help system' (without quotes) for an introduction to",
" the help system.\n",
" '?chapters' and '?sections' will display tables of contents."
];
Pager(lines);
return true;
end);
#############################################################################
##
#F HELP_GET_MATCHES( <book>, <topic>, <frombegin> ) . . . search through
#F the books
##
## This function returns a list of two lists [exact, match] and these lists
## consist of pairs [book, entrynumber], where book is a help book and
## entrynumber is the number of a match in book.entries. As the names say,
## the first list "exact" contains the exact matches and "match" the
## remaining ones.
##
InstallGlobalFunction(HELP_GET_MATCHES,
function( books, topic, frombegin )
local exact, match, em, b, x, topics, getsecnum;
# First we try to produce some suggestions for possible different spellings
# (see the global variable 'TRANSATL' for the list of spelling patterns).
if topic =
"size" then # "size" is a notable exception (lowercase is guaranteed)
topics:=[ topic ];
else
topics:=HELP_SEARCH_ALTERNATIVES( topic );
fi;
# <exact> and <match> contain the topics matching
exact := [];
match := [];
if IsString(books) or IsRecord(books)
then
books := [books];
fi;
# collect the matches (by number)
books := List(books, HELP_BOOK_INFO);
for b in books
do
for topic in topics
do
# now delegate the work to the handler functions
if b<>fail
then
em := HELP_BOOK_HANDLER.(b.handler).SearchMatches(b, topic, frombegin);
for x in em[
1]
do
Add(exact, [b, x]);
od;
for x in em[
2]
do
Add(match, [b, x]);
od;
fi;
od;
od;
# we now join the two lists, this way the exact matches are displayed
# first in case of multiple matches
# Note: before GAP 4.5 this was only done in case of substring search.
match := Concatenation(exact, match);
exact := [];
# check if all matches point to the same subsection of the same book,
# in that case we only keep the first match which then will be displayed
# immediately
# this function makes sure that nothing breaks if the help book handler
# has no support for SubsectionNumber
getsecnum :=
function(m)
if IsBound(HELP_BOOK_HANDLER.(m[
1].handler).SubsectionNumber)
then
return HELP_BOOK_HANDLER.(m[
1].handler).SubsectionNumber(m[
1], m[
2]);
else
return m[
2];
fi;
end;
if Length(match) >
1 and Length(Set(match,
m-> [m[
1].bookname,getsecnum(m)])) =
1 then
match := [match[
1]];
fi;
return [exact, match];
end);
#############################################################################
##
#F InitialSubstringUTF8Text( <str>, <cols> )
##
## This is a utility that extends the GAPDoc function
## <C>InitialSubstringUTF8String</C>, which deals with strings containing
## unicode characters but does not deal with escape sequences, i.e.,
## sequences starting with ESC and stopping with the first letter
## afterwards).
## Note that the text version of GAPDoc manuals contains both
## unicode characters and escape sequences.
##
## <Ref Func="InitialSubstringUTF8Text"/> returns a string that is the
## longest prefix of the string <A>str</A> that has visible/printed length
## at most <A>cols</A> and contains all escape sequences from <C>str</C>.
##
# The following global variables will be defined via the GAPDoc package.
# We assign them here (and unbind them later on) in order to avoid syntax
# warnings.
if not IsBound( InitialSubstringUTF8String )
then
InitialSubstringUTF8String:=
"dummy";
fi;
if not IsBound( WidthUTF8String )
then
WidthUTF8String:=
"dummy";
fi;
BindGlobal(
"InitialSubstringUTF8Text",
function( str, cols )
local esc, len, res, j, pos, word, w;
esc:= CHAR_INT(
27);
len:= Length( str );
res:=
"";
j:=
0;
while true
do
pos:= Position( str, esc, j );
if pos = fail
then
pos:= len+
1;
fi;
word:= str{ [ j+
1 .. pos-
1 ] };
w:= WidthUTF8String( word );
if w <= cols
then
Append( res, word );
cols:= cols - w;
elif cols >
0 then
Append( res, InitialSubstringUTF8String( word, cols ) );
cols:=
0;
fi;
if len < pos
then
break;
fi;
# Now pos points at an ESC character; all escape sequences we
# support are terminated by a letter, so search for one.
j:= PositionProperty( str, c -> c in CHARS_ALPHA, pos );
if j = fail
then
Error(
"string end inside escape sequence" );
fi;
Append( res, str{ [ pos .. j ] } );
od;
return res;
end );
if not IsReadOnlyGlobal(
"InitialSubstringUTF8String" )
then
Unbind( InitialSubstringUTF8String );
fi;
if not IsReadOnlyGlobal(
"WidthUTF8String" )
then
Unbind( WidthUTF8String );
fi;
#############################################################################
##
#F HELP_SHOW_MATCHES( <book>, <topic>, <frombegin> ) . . . show list of
#F matches or single match directly
##
InstallGlobalFunction(HELP_SHOW_MATCHES,
function( books, topic, frombegin )
local exact, match, x, lines, cnt, i, str, n, width, line;
# first get lists of exact and other matches
x := HELP_GET_MATCHES( books, topic, frombegin );
exact := x[
1];
match := x[
2];
# no topic found
if 0 = Length(match) and
0 = Length(exact)
then
Print(
"Help: no matching entry found\n" );
return false;
# one exact or together one topic found
elif 1 = Length(exact) or (
0 = Length(exact) and
1 = Length(match))
then
if Length(exact) =
0 then exact := match;
fi;
i := exact[
1];
str := Concatenation(
"Help: Showing `", i[
1].bookname,
": ",
i[
1].entries[i[
2]][
1],
"'\n");
# to avoid line breaking when str contains escape sequences:
n :=
0;
while n < Length(str)
do
Print(str{[n+
1..Minimum(Length(str),
n + QuoInt(SizeScreen()[
1] ,
2))]},
"\c");
n := n + QuoInt(SizeScreen()[
1] ,
2);
od;
HELP_PRINT_MATCH(i);
return true;
# more than one topic found, show overview in pager
else
lines :=
[
"Help: several entries match this topic - type ?2 to get match [2]"];
HELP_LAST.TOPICS:=[];
cnt :=
0;
# show exact matches first
match := Concatenation(exact, match);
width:= SizeScreen()[
1];
for i in match
do
cnt := cnt+
1;
topic := Concatenation(i[
1].bookname,
": ",i[
1].entries[i[
2]][
1]);
Add(HELP_LAST.TOPICS, i);
line:= Concatenation(
"[",String(cnt),
"] ",topic);
Add(lines, InitialSubstringUTF8Text(line, width));
od;
Pager(rec(lines := lines, formatted := true));
return true;
fi;
end);
# choosing one of last shown list of matches
InstallGlobalFunction(HELP_SHOW_FROM_LAST_TOPICS,
function(nr)
if nr =
0 or Length(HELP_LAST.TOPICS) < nr
then
Print(
"Help: No such topic.\n");
return false;
fi;
HELP_PRINT_MATCH(HELP_LAST.TOPICS[nr]);
return true;
end);
## A generic function for HELP_BOOK_HANDLER.(handler).HelpData(b, e, "ref")
## This can be used to resolve cross references between books. The function
## returns a list r with six entries:
##
## - r[1] search string including book name "ref: Xyz"
## - r[2] (sub)section number as string "5.14.2" or "3.1"
## - r[3] name of dvi-file (or fail) "/doc/path/manual.dvi"
## - r[4] name of pdf-file (or fail) "/doc/path/manual.pdf"
## - r[5] page number for r[3], r[4] (or fail) 37
## - r[6] URL (or fail) "/doc/htm/ch3.html#s4"
## - r[7] [chnr, secnr, subsecnr] [5,14,2] or [3,1,0]
##
HELP_BOOK_HANDLER.HelpDataRef :=
function(book, entrynr)
local info, handler, entry, secnr, res, r;
info := HELP_BOOK_INFO(book);
handler := info.handler;
entry := info.entries[entrynr];
# the search and reference string
res := [ Concatenation(info.bookname,
": ", entry[
1]) ];
# the section number string
secnr := HELP_BOOK_HANDLER.(handler).HelpData(info, entrynr,
"secnr");
Add(res, secnr[
2]);
# dvi-file and page number
r := HELP_BOOK_HANDLER.(handler).HelpData(info, entrynr,
"dvi");
if r = fail
then
Add(res, fail);
else
Add(res, r.file);
res[
5] := r.page;
fi;
# pdf-file and page number
r := HELP_BOOK_HANDLER.(handler).HelpData(info, entrynr,
"pdf");
if r = fail
then
res[
4] := fail;
else
res[
4] := r.file;
if not IsBound(res[
5])
then
res[
5] := r.page;
fi;
fi;
if not IsBound(res[
5])
then
res[
5] := fail;
fi;
# URL
r := HELP_BOOK_HANDLER.(handler).HelpData(info, entrynr,
"url");
if r = fail
then
Add(res, fail);
else
Add(res, r);
fi;
# [chnr, secnr, subsecnr] as list
Add(res, secnr[
1]);
return res;
end;
## From this info we generate a manual.lab file for a given book.
## Note that the gapmacro format has subsection information in manual.lab
## which is not available in manual.six! So, one cannot properly regenerate
## a manual.lab file for those books.
InstallGlobalFunction(HELP_LAB_FILE,
function(file, book)
local fun, str, i;
book := HELP_BOOK_INFO(book);
fun :=
function(i)
local r;
r := HELP_BOOK_HANDLER.HelpDataRef(book, i);
return Concatenation(
"\\makelabel {",
SIMPLE_STRING(r[
1]),
"}{", r[
2],
"}\n");
end;
str :=
"";
for i in [
1..Length(book.entries)]
do
Append(str, fun(i));
od;
PrintTo(file, str);
end);
#############################################################################
##
#F HELP( <string> ) . . . . . . . . . . . . . . . deal with a help request
##
# here we store the last 16 requests
HELP_RING_IDX :=
0;
HELP_RING_SIZE :=
16;
BindGlobal(
"HELP_BOOK_RING", ListWithIdenticalEntries( HELP_RING_SIZE,
[
"tutorial"] ));
if IsHPCGAP
then
LockAndMigrateObj(HELP_BOOK_RING,HELP_REGION);
fi;
BindGlobal(
"HELP_TOPIC_RING", ListWithIdenticalEntries( HELP_RING_SIZE,
"welcome to gap" ));
if IsHPCGAP
then
LockAndMigrateObj(HELP_TOPIC_RING,HELP_REGION);
fi;
BindGlobal(
"HELP_ORIG_TOPIC_RING", ListWithIdenticalEntries( HELP_RING_SIZE,
"welcome to gap" ));
if IsHPCGAP
then
LockAndMigrateObj(HELP_ORIG_TOPIC_RING,HELP_REGION);
fi;
# here we store the last shown topic, initialized with 0 (leading to
# show "Tutorial: Help", see below)
BindGlobal(
"HELP_LAST", rec(MATCH :=
0, BOOK :=
0,
NEXT_VIEWER := false, TOPICS := []));
NAMES_SYSTEM_GVARS:=
"to be defined in init.g";
InstallGlobalFunction(HELP,
function( str )
local origstr, nwostr, p, book, books, move, add;
origstr := ShallowCopy(str);
while Last( origstr ) =
';' do
Remove( origstr );
od;
nwostr := NormalizedWhitespace(origstr);
# extract the book
p := Position( str,
':' );
if p <> fail
then
book := str{[
1..p-
1]};
str := str{[p+
1..Length(str)]};
else
book :=
"";
fi;
# normalizing for search
book := SIMPLE_STRING(book);
str := SIMPLE_STRING(str);
# we check if `book' MATCH_BEGINs some of the available books
books := Filtered(HELP_KNOWN_BOOKS[
1], bn-> MATCH_BEGIN(bn, book));
if Length(book) >
0 and Length(books) =
0 then
Print(
"Help: None of the available books matches (try: '?books').\n");
return;
fi;
# function to add a topic to the ring
move := false;
add :=
function( books, topic, orig_topic )
if not move
then
HELP_RING_IDX := (HELP_RING_IDX+
1) mod HELP_RING_SIZE;
HELP_BOOK_RING[HELP_RING_IDX+
1] := books;
HELP_TOPIC_RING[HELP_RING_IDX+
1] := topic;
HELP_ORIG_TOPIC_RING[HELP_RING_IDX+
1] := orig_topic;
fi;
end;
# if the topic is empty show the last shown one again
if book =
"" and str =
"" then
if HELP_LAST.BOOK =
0 then
HELP(
"Tutorial: Help");
else
HELP_PRINT_MATCH( [HELP_LAST.BOOK, HELP_LAST.MATCH] );
fi;
return;
# if topic is "&" show last topic again, but with next viewer in viewer
# list, or with last viewer again if there is no next one
elif book =
"" and str =
"&" and Length(nwostr) =
1 then
if HELP_LAST.BOOK =
0 then
HELP(
"Tutorial: Help");
else
HELP_LAST.NEXT_VIEWER := true;
HELP_PRINT_MATCH( [HELP_LAST.BOOK, HELP_LAST.MATCH] );
fi;
return;
# if the topic is '-' we are interested in the previous search again
elif book =
"" and str =
"-" and Length(nwostr) =
1 then
HELP_RING_IDX := (HELP_RING_IDX-
1) mod HELP_RING_SIZE;
books := HELP_BOOK_RING[HELP_RING_IDX+
1];
str := HELP_TOPIC_RING[HELP_RING_IDX+
1];
origstr := HELP_ORIG_TOPIC_RING[HELP_RING_IDX+
1];
move := true;
# if the topic is '+' we are interested in the last section again
elif book =
"" and str =
"+" and Length(nwostr) =
1 then
HELP_RING_IDX := (HELP_RING_IDX+
1) mod HELP_RING_SIZE;
books := HELP_BOOK_RING[HELP_RING_IDX+
1];
str := HELP_TOPIC_RING[HELP_RING_IDX+
1];
origstr := HELP_ORIG_TOPIC_RING[HELP_RING_IDX+
1];
move := true;
fi;
# number means topic from HELP_LAST.TOPICS list
if book =
"" and ForAll(str, a-> a in
"0123456789")
then
HELP_SHOW_FROM_LAST_TOPICS(Int(str));
# if the topic is '<' we are interested in the one before 'LastTopic'
elif book =
"" and str =
"<" and Length(nwostr) =
1 then
HELP_SHOW_PREV();
# if the topic is '>' we are interested in the one after 'LastTopic'
elif book =
"" and str =
">" and Length(nwostr) =
1 then
HELP_SHOW_NEXT();
# if the topic is '<<' we are interested in the previous chapter intro
elif book =
"" and str =
"<<" then
HELP_SHOW_PREV_CHAPTER();
# if the topic is '>>' we are interested in the next chapter intro
elif book =
"" and str =
">>" then
HELP_SHOW_NEXT_CHAPTER();
# if the subject is 'Welcome to GAP' display a welcome message
elif book =
"" and str =
"welcome to gap" then
if HELP_SHOW_WELCOME(book)
then
add( books,
"Welcome to GAP",
"Welcome to GAP" );
fi;
# if the topic is 'books' display the table of books
elif book =
"" and str =
"books" then
if HELP_SHOW_BOOKS()
then
add( books,
"books",
"books" );
fi;
# if the topic is 'chapters' display the table of chapters
elif str =
"chapters" or str =
"contents" or book <>
"" and str =
"" then
if ForAll(books, HELP_SHOW_CHAPTERS)
then
add( books,
"chapters",
"chapters" );
fi;
# if the topic is 'sections' display the table of sections
elif str =
"sections" then
if ForAll(books, HELP_SHOW_SECTIONS)
then
add(books,
"sections",
"sections");
fi;
# if the topic is '?<string>' search the index for any entries for
# which <string> is a substring (as opposed to an abbreviation)
elif Length(str) >
0 and str[
1] =
'?' then
str := str{[
2..Length(str)]};
NormalizeWhitespace(str);
origstr := origstr{[
2..Length(origstr)]};
if HELP_SHOW_MATCHES( books, str, false : HELP_TOPIC:= origstr )
then
add( books, str, origstr );
fi;
# search for this topic
elif HELP_SHOW_MATCHES( books, str, true : HELP_TOPIC:= origstr )
then
add( books, str, origstr );
elif origstr in NAMES_SYSTEM_GVARS
then
Print(
"Help: '", origstr,
"' is currently undocumented.\n",
" For details, try ?Undocumented Variables\n" );
elif book =
"" and
ForAny(HELP_KNOWN_BOOKS[
1], bk -> MATCH_BEGIN(bk, str))
then
Print(
"Help: Are you looking for a certain book? (Trying '?", origstr,
":' ...\n");
HELP( Concatenation(origstr,
":") );
else
# seems unnecessary, since some message is already printed in all
# cases above (?):
# Print( "Help: Sorry, could not find a match for '", origstr, "'.\n");
fi;
end);