#############################################################################
##
#W  Text.gi                      GAPDoc                          Frank Lübeck
##
##
#Y  Copyright (C)  2000,  Frank Lübeck,  Lehrstuhl D für Mathematik,  
#Y  RWTH Aachen
##
##  The files Text.g{d,i}  contain some utilities for  dealing with text
##  strings.
##  

##  
##  <#GAPDoc Label="CharsColls">
##  <ManSection>
##  <Var Name="WHITESPACE" />
##  <Var Name="CAPITALLETTERS" />
##  <Var Name="SMALLLETTERS" />
##  <Var Name="LETTERS" />
##  <Var Name="DIGITS" />
##  <Var Name="HEXDIGITS" />
##  <Var Name="BOXCHARS" />
##  <Description>
##  These variables contain sets of characters which are useful for
##  text processing. They are defined as follows.<P/>
##  <List >
##  <Mark><C>WHITESPACE</C></Mark>
##  <Item><C>" \n\t\r"</C></Item>
##  <Mark><C>CAPITALLETTERS</C></Mark>
##  <Item><C>"ABCDEFGHIJKLMNOPQRSTUVWXYZ"</C></Item>
##  <Mark><C>SMALLLETTERS</C></Mark>
##  <Item><C>"abcdefghijklmnopqrstuvwxyz"</C></Item>
##  <Mark><C>LETTERS</C></Mark>
##  <Item>concatenation of <C>CAPITALLETTERS</C> and <C>SMALLLETTERS</C></Item>
##  <Mark><C>DIGITS</C></Mark><Item><C>"0123456789"</C></Item>
##  <Mark><C>HEXDIGITS</C></Mark><Item><C>"0123456789ABCDEFabcdef"</C></Item>
##  <Mark><C>BOXCHARS</C></Mark>
##     <Item><Alt Not="LaTeX"><C>"─│┌┬┐├┼┤└┴┘━┃┏┳┓┣╋┫┗┻┛═║╔╦╗╠╬╣╚╩╝"</C></Alt>
##     <Alt Only="LaTeX"><C>Encode(Unicode(9472 + [ 0, 2, 12, 44, 16, 28,
##     60, 36, 20, 52, 24, 1, 3, 15, 51, 19, 35, 75, 43, 23, 59, 27, 80, 81,
##     84, 102, 87, 96, 108, 99, 90, 105, 93 ]), "UTF-8")</C></Alt>, 
##  these are  in UTF-8 encoding,  the <C>i</C>-th unicode  character is
##  <C>BOXCHARS{[3*i-2..3*i]}</C>.</Item>
##  </List>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  
InstallValue(WHITESPACE, " \n\t\r");
InstallValue(CAPITALLETTERS, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
IsSet(CAPITALLETTERS);
InstallValue(SMALLLETTERS, "abcdefghijklmnopqrstuvwxyz");
IsSet(SMALLLETTERS);
InstallValue(LETTERS, Concatenation(CAPITALLETTERS, SMALLLETTERS));
IsSet(LETTERS);
InstallValue(DIGITS, "0123456789");
InstallValue(HEXDIGITS, "0123456789ABCDEFabcdef");
InstallValue(BOXCHARS, "─│┌┬┐├┼┤└┴┘━┃┏┳┓┣╋┫┗┻┛═║╔╦╗╠╬╣╚╩╝");

MakeImmutable(WHITESPACE);
MakeImmutable(CAPITALLETTERS);
MakeImmutable(SMALLLETTERS);
MakeImmutable(LETTERS);
MakeImmutable(DIGITS);
MakeImmutable(HEXDIGITS);
MakeImmutable(BOXCHARS);

# utilities to find lines
InstallGlobalFunction(PositionLinenumber, function(str, nr)
  local pos, i;
  pos := 0;
  i := 1;
  while i < nr and pos <> fail do
    pos := Position(str, '\n', pos);
    i := i+1;
  od;
  if i = nr and IsInt(pos) then
    return pos+1;
  else
    return fail;
  fi;
end);
InstallGlobalFunction(NumberOfLines, function(str)
  local pos, i;
  if Length(str) = 0 then
    return 0;
  fi;
  pos := 0;
  i := 1;
  while pos <> fail do
    pos := Position(str, '\n', pos);
    i := i+1;
  od;
  if str[Length(str)] = '\n' then
    return i-2;
  else
    return i-1;
  fi;
end);

##  
##  <#GAPDoc Label="TextAttr">
##  <ManSection>
##  <Var Name="TextAttr" />
##  <Description>
##  The  record  <Ref Var="TextAttr"/>  contains  strings  which can  be
##  printed  to   change  the  terminal  attribute   for  the  following
##  characters. This  only works  with terminals which  understand basic
##  ANSI escape sequences.  Try the following example to see  if this is
##  the case for the terminal you are  using. It shows the effect of the
##  foreground and background color  attributes and of the <C>.bold</C>,
##  <C>.blink</C>, <C>.normal</C>, <C>.reverse</C> and <C>.underscore</C>
##  which can partly be mixed.
##  
##  <Listing Type="Example">
##  extra := ["CSI", "reset", "delline", "home"];;
##  for t in Difference(RecNames(TextAttr), extra) do
##    Print(TextAttr.(t), "TextAttr.", t, TextAttr.reset,"\n");
##  od;
##  </Listing>
##  
##  The suggested defaults for colors <C>0..7</C> are black, red, green,
##  brown, blue,  magenta, cyan,  white. But this  may be  different for
##  your terminal configuration.<P/>
##  
##  The  escape  sequence <C>.delline</C>  deletes  the  content of  the
##  current line and  <C>.home</C> moves the cursor to  the beginning of
##  the current line.
##  
##  <Listing Type="Example">
##  for i in [1..5] do 
##    Print(TextAttr.home, TextAttr.delline, String(i,-6), "\c"); 
##    Sleep(1); 
##  od;
##  </Listing>
##  
##  <Index>UseColorsInTerminal</Index> 
##  Whenever  you  use  this  in   some  printing  routines  you  should
##  make  it optional.  Use  these attributes  only  when 
##  <C>UserPreference("UseColorsInTerminal");</C> returns <K>true</K>.
##  </Description>
##  </ManSection>
##  
##  <#/GAPDoc>
##  
InstallValue(TextAttr, rec());
TextAttr.CSI := "\033[";
TextAttr.reset := Concatenation(TextAttr.CSI, "0m");
TextAttr.normal := Concatenation(TextAttr.CSI, "22m");
TextAttr.bold := Concatenation(TextAttr.CSI, "1m");
TextAttr.underscore := Concatenation(TextAttr.CSI, "4m");
TextAttr.blink := Concatenation(TextAttr.CSI, "5m");
TextAttr.reverse := Concatenation(TextAttr.CSI, "7m");
# foreground colors 0..7 (default: black, red, green, brown, blue, magenta,
# cyan, white
TextAttr.0 := Concatenation(TextAttr.CSI, "30m");
TextAttr.1 := Concatenation(TextAttr.CSI, "31m");
TextAttr.2 := Concatenation(TextAttr.CSI, "32m");
TextAttr.3 := Concatenation(TextAttr.CSI, "33m");
TextAttr.4 := Concatenation(TextAttr.CSI, "34m");
TextAttr.5 := Concatenation(TextAttr.CSI, "35m");
TextAttr.6 := Concatenation(TextAttr.CSI, "36m");
TextAttr.7 := Concatenation(TextAttr.CSI, "37m");
# background colors 0..7
TextAttr.b0 := Concatenation(TextAttr.CSI, "40m");
TextAttr.b1 := Concatenation(TextAttr.CSI, "41m");
TextAttr.b2 := Concatenation(TextAttr.CSI, "42m");
TextAttr.b3 := Concatenation(TextAttr.CSI, "43m");
TextAttr.b4 := Concatenation(TextAttr.CSI, "44m");
TextAttr.b5 := Concatenation(TextAttr.CSI, "45m");
TextAttr.b6 := Concatenation(TextAttr.CSI, "46m");
TextAttr.b7 := Concatenation(TextAttr.CSI, "47m");

TextAttr.delline := Concatenation(TextAttr.CSI, "2K");
TextAttr.home := Concatenation(TextAttr.CSI, "1G");

MakeImmutable(TextAttr);

##  <#GAPDoc Label="RepeatedString">
##  <ManSection >
##  <Func Arg="c, len" Name="RepeatedString" />
##  <Func Arg="c, len" Name="RepeatedUTF8String" />
##  <Description>
##  Here <A>c</A> must be either a  character or a string and <A>len</A>
##  is a non-negative number. Then <Ref Func="RepeatedString" /> returns
##  a string of length <A>len</A> consisting of copies of <A>c</A>.
##  <P/>
##  In the variant <Ref Func="RepeatedUTF8String" /> the argument <A>c</A>
##  is considered as string in UTF-8 encoding, and it can also be specified
##  as unicode string or character, see <Ref Oper="Unicode" />. The result is 
##  a string in UTF-8 encoding which has visible width <A>len</A> as explained
##  in <Ref Func="WidthUTF8String"/>. 
##  <Example>
##  gap> RepeatedString('=',51);
##  "==================================================="
##  gap> RepeatedString("*=",51);
##  "*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*"
##  gap> s := "bäh";;
##  gap> enc := GAPInfo.TermEncoding;;
##  gap> if enc &lt;&gt; "UTF-8" then s := Encode(Unicode(s, enc), "UTF-8"); fi;
##  gap> l := RepeatedUTF8String(s, 8);;
##  gap> u := Unicode(l, "UTF-8");;
##  gap> Print(Encode(u, enc), "\n");
##  bähbähbä
##  </Example>
##  </Description>
##  </ManSection>
##  
##  <#/GAPDoc>
InstallGlobalFunction(RepeatedString, function(s, n)
  local res, i;
  res := EmptyString(n+1);
  if n = 0 then
    return res;
  elif IsString(s) and Length(s) > 0 then
    if Length(s) > n then
      for i in [1..n] do
        res[i] := s[i];
      od;
      return s;
    else
      Append(res, s);
    fi;
  elif IsChar(s) then
    res[1] := s;
  else
    Error("First argument must be character or non-empty string\n");
  fi;
  while 2*Length(res) <= n do
    Append(res, res);
  od;
  Append(res, res{[1..n-Length(res)]});
  return res;
end);
InstallGlobalFunction(RepeatedUTF8String, function(s, n)
  local res, w, r, u, tail, i;
  if IsUnicodeCharacter(s) then
    # need to check this first because s is also in IsChar
    s := Encode(Unicode([Int(s)]),"utf8");
  elif IsChar(s) then
    res := "";
    Add(res,s);
    s := res;
  elif IsUnicodeString(s) then
    s := Encode(s, "utf8");
  elif not IsString(s) then
    Error("RepeatedUTF8String: First argument must be character, string \
or unicode \ncharacter or string.\n"); 
  fi;
  w := WidthUTF8String(s);
  if w = 0 then
    if n = 0 then 
      return "";
    else
      Error("RepeatedUTF8String: First argument has width 0.\n"); 
    fi;
  fi;
  r := QuotientRemainder(n, w);
  if r[2] <> 0 then
    u := Unicode(s, "utf8");
    tail := "";
    i := 1;
    while WidthUTF8String(tail) < r[2] do
      Append(tail, Encode(u{[i]}, "utf8"));
      i := i+1;
    od;
  else
    tail := "";
  fi;
  if r[1] = 0 then
    return tail;
  fi;
  r := r[1]*Length(s)+Length(tail);
  res := EmptyString(r);
  Append(res, s);
  while 2*Length(res) <= r do
    Append(res, res);
  od;
  Append(res, res{[1..r-Length(tail)-Length(res)]});
  Append(res, tail);
  return res;
end);


##  <#GAPDoc Label="PositionMatchingDelimiter">
##  <ManSection >
##  <Func Arg="str, delim, pos" Name="PositionMatchingDelimiter" />
##  <Returns>position as integer or <K>fail</K></Returns>
##  <Description>
##  Here <A>str</A> must  be a string and <A>delim</A>  a string with
##  two  different characters.  This function  searches the  smallest
##  position   <C>r</C>  of   the  character   <C><A>delim</A>[2]</C>
##  in   <A>str</A>   such  that   the   number   of  occurrences  of
##  <C><A>delim</A>[2]</C>    in    <A>str</A>   between    positions
##  <C><A>pos</A>+1</C>  and  <C>r</C> is  by  one  greater than  the
##  corresponding number of occurrences of <C><A>delim</A>[1]</C>.<P/>
##  
##  If such an <C>r</C> exists, it is returned. Otherwise <K>fail</K>
##  is returned.
##  
##  <Example>
##  gap> PositionMatchingDelimiter("{}x{ab{c}d}", "{}", 0);
##  fail
##  gap> PositionMatchingDelimiter("{}x{ab{c}d}", "{}", 1);
##  2
##  gap> PositionMatchingDelimiter("{}x{ab{c}d}", "{}", 6);
##  11
##  </Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
InstallGlobalFunction(PositionMatchingDelimiter, function(str, delim, pos)
  local   b,  e,  p,  l,  level;

  b := delim[1];
  e := delim[2];
  
  p := pos+1;
  l := Length(str);
  level := 0;
  while true do
    if p > l then
      return fail;
    elif str[p] = b then
      level := level+1;
    elif str[p] = e then
      if level = 0 then 
        return p;
      else
        level := level-1;
      fi;
    fi;
    p := p+1;
  od;
end);

##  <#GAPDoc Label="SubstitutionSublist">
##  <ManSection >
##  <Func Arg="list, sublist, new[, flag]" Name="SubstitutionSublist" />
##  <Returns>the changed list</Returns>
##  <Description>
##  This function looks for (non-overlapping) occurrences of a sublist
##  <A>sublist</A> in a list <A>list</A> (compare <Ref BookName="ref"
##  Oper="PositionSublist"  />) and  returns a  list where  these are
##  substituted with the list <A>new</A>.<P/>
##  
##  The  optional argument  <A>flag</A>  can  either be  <C>"all"</C>
##  (this is the default if not given) or <C>"one"</C>. In the second
##  case only  the first occurrence of <A>sublist</A> is substituted.
##  <P/>
##  
##  If <A>sublist</A> does not  occur in <A>list</A> then <A>list</A>
##  itself is returned (and not a <C>ShallowCopy(list)</C>).
##  
##  <Example>
##  gap> SubstitutionSublist("xababx", "ab", "a");
##  "xaax"
##  </Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
InstallGlobalFunction(SubstitutionSublist, function(arg)
  local   str,  substr,  lss,  subs,  all,  p,  s, off;
  str := arg[1];
  substr := arg[2];
  lss := Length(substr);
  subs := arg[3];
  if Length(arg)>3 then
    all := arg[4]="all";
  else
    all := true;
  fi;
  
  p := PositionSublist(str, substr);
  if p = fail then 
    # return original object in case of no substitution
    return str; 
  fi;
  s := str{[]};
  off := 1-lss;
  while p<>fail do
    Append(s, str{[off+lss..p-1]});
    Append(s, subs);
##      str := str{[p+lss..Length(str)]};
    off := p;
    if all then
      p := PositionSublist(str, substr, p+lss-1);
    else
      p := fail;
    fi;
    if p=fail then
      Append(s, str{[off+lss..Length(str)]});
    fi;
  od;
  return s;
end);
    

##  <#GAPDoc Label="NumberDigits">
##  <ManSection >
##  <Func Arg="str, base" Name="NumberDigits" />
##  <Returns>integer</Returns>
##  <Func Arg="n, base" Name="DigitsNumber" />
##  <Returns>string</Returns>
##  <Description>
##  The argument  <A>str</A> of  <Ref Func="NumberDigits" />  must be
##  a  string  consisting  only  of an  optional  leading  <C>'-'</C>
##  and characters  in  <C>0123456789abcdefABCDEF</C>,  describing an
##  integer  in  base <A>base</A>  with  <M>2  \leq <A>base</A>  \leq
##  16</M>. This function returns the corresponding integer.<P/>
##  
##  The function <Ref Func="DigitsNumber" /> does the reverse.
##  
##  <Example>
##  gap> NumberDigits("1A3F",16);
##  6719
##  gap> DigitsNumber(6719, 16);
##  "1A3F"
##  </Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  
InstallGlobalFunction(NumberDigits, function(str, base)
  local   res,  x,  nr, sign;
  res := 0;
  sign := 1;
  for x in str do
    nr := INT_CHAR(x) - 48;
    if nr = -3 then 
      # '-'
      sign := -sign;
    else
      if nr>48 then 
        nr := nr - 39;
      elif nr>15 then
        nr := nr - 7;
      fi;
      res := res*base + nr;
    fi;
  od;
  return sign*res;
end);

InstallGlobalFunction(DigitsNumber, function(n, base)
  local str, s;
  s := "";
  if n<0 then
    Add(s, '-');
    n := -n;
  fi;
  str := "";
  while n <> 0 do
    Add(str, HEXDIGITS[(n mod base) + 1]);
    n := QuoInt(n, base);
  od;
  return Concatenation(s, Reversed(str));
end);

##  <#GAPDoc Label="LabelInt">
##  <ManSection >
##  <Func Arg="n, type, pre, post" Name="LabelInt" />
##  <Returns>string</Returns>
##  <Description>
##  The argument <A>n</A> must be an integer in the range from 1 to 5000,
##  while <A>pre</A> and <A>post</A> must be strings.
##  <P/>
##  The argument <A>type</A> can be one of <C>"Decimal"</C>,
##  <C>"Roman"</C>, <C>"roman"</C>, <C>"Alpha"</C>, <C>"alpha"</C>.
##  <P/>
##  The function returns a string that starts with <A>pre</A>, followed by
##  a decimal, respectively roman number or alphanumerical number literal
##  (capital, respectively small letters), followed by <A>post</A>.
##  <P/>
##  <Example>
##  gap> List([1,2,3,4,5,691], i-> LabelInt(i,"Decimal","","."));
##  [ "1.", "2.", "3.", "4.", "5.", "691." ]
##  gap> List([1,2,3,4,5,691], i-> LabelInt(i,"alpha","(",")"));
##  [ "(a)", "(b)", "(c)", "(d)", "(e)", "(zo)" ]
##  gap> List([1,2,3,4,5,691], i-> LabelInt(i,"alpha","(",")"));
##  [ "(a)", "(b)", "(c)", "(d)", "(e)", "(zo)" ]
##  gap> List([1,2,3,4,5,691], i-> LabelInt(i,"Alpha","",".)"));
##  [ "A.)", "B.)", "C.)", "D.)", "E.)", "ZO.)" ]
##  gap> List([1,2,3,4,5,691], i-> LabelInt(i,"roman","","."));
##  [ "i.", "ii.", "iii.", "iv.", "v.", "dcxci." ]
##  gap> List([1,2,3,4,5,691], i-> LabelInt(i,"Roman","",""));
##  [ "I", "II", "III", "IV", "V", "DCXCI" ]
##  </Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  
InstallGlobalFunction(LabelInt, function(n, type, pre, post)
  local l1, l2, l3, l, res, r, i;
  if not IsInt(n) or n < 1 or n>5000 then
    return fail;
  fi;
  if type="roman" then
    l1 := ["","i","ii","iii","iv","v","vi","vii","viii","ix"];
    l2 := ["","x","xx","xxx","xl","l","lx","lxx","lxxx","xc"];
    l3 := ["","c","cc","ccc","cd","d","dc","dcc","dccc","cm","m"];
  elif type="Roman" then
    l1 := ["","I","II","III","IV","V","VI","VII","VIII","IX"];
    l2 := ["","X","XX","XXX","XL","L","LX","LXX","LXXX","XC"];
    l3 := ["","C","CC","CCC","CD","D","DC","DCC","DCCC","CM","M"];
  fi;
  if type="alpha" then
    l := LETTERS{[27..52]};
  elif type="Alpha" then
    l := LETTERS{[1..26]};
  fi;
  if type="Decimal" then
    res := String(n);
  elif type in ["roman","Roman"] then
    res := "";
    for i in [1..QuoInt(n,1000)] do
      Append(res, l3[11]);
    od;
    Append(res, l3[QuoInt(n,100) mod 10 + 1]);
    Append(res, l2[QuoInt(n,10) mod 10 + 1]);
    Append(res, l1[n mod 10 + 1]);
  elif type in ["alpha", "Alpha"] then
    if n < 27 then
      res := l{[n]};
    elif n <= 26*27 then
      res := l{[QuoInt(n-1,26),((n-1) mod 26)+1]};
    else
      res := l{[QuoInt(n-27,26*26),
                QuoInt((n-27) mod 26^2-1, 26)+1,((n-1) mod 26)+1]};
    fi;
  fi;
  return Concatenation(pre, res, post);
end);
 
##  <#GAPDoc Label="StripBeginEnd">
##  <ManSection >
##  <Func Arg="list, strip" Name="StripBeginEnd" />
##  <Returns>changed string</Returns>
##  <Description>
##  Here <A>list</A>  and <A>strip</A>  must be lists.  This function
##  returns the  sublist of list  which does not contain  the leading
##  and trailing  entries which are  entries of <A>strip</A>.  If the
##  result  is  equal  to  <A>list</A>  then  <A>list</A>  itself  is
##  returned.
##  
##  <Example>
##  gap> StripBeginEnd(" ,a, b,c,   ", ", ");
##  "a, b,c"
##  </Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  
InstallGlobalFunction(StripBeginEnd, function(str, chars)
  local   pb,  l,  pe;
  pb := 1;
  l := Length(str);
  while  pb <= l and str[pb] in chars do
    pb := pb + 1;
  od;
  pe := l;
  while pe > 0 and str[pe] in chars do
    pe := pe - 1;
  od;
  if pb > 1 or pe < l then
    return str{[pb..pe]};
  else
    return str;
  fi;
end);


##  <#GAPDoc Label="NormalizedWhitespace">
##  <ManSection >
##  <Func Arg="str" Name="NormalizedWhitespace" />
##  <Returns>new string with white space normalized</Returns>
##  <Description>
##  This  function  gets  a  string  <A>str</A>  and  returns  a  new
##  string  which  is a  copy  of  <A>str</A> with  normalized  white
##  space.  Note  that  the   library  function  <Ref  BookName="ref"
##  Func="NormalizeWhitespace"  />  works in place  and  changes  its
##  argument.
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  
# moved into GAP library
##  InstallGlobalFunction(NormalizedWhitespace, function(str)
##    local   res;
##    res := ShallowCopy(str);
##    NormalizeWhitespace(res);
##    return res;
##  end);



##  <#GAPDoc Label="WrapTextAttribute">
##  <ManSection >
##  <Func Arg="str, attr" Name="WrapTextAttribute" />
##  <Returns>a string with markup</Returns>
##  <Description>
##  The argument <A>str</A> must be a text as &GAP; string, possibly with 
##  markup by escape sequences as in <Ref Var="TextAttr" />. This function
##  returns a string which is wrapped by the escape sequences <A>attr</A>
##  and <C>TextAttr.reset</C>. It takes care of markup in the given string
##  by appending <A>attr</A> also after each given <C>TextAttr.reset</C> in
##  <A>str</A>.
##  <Example>
##  gap> str := Concatenation("XXX",TextAttr.2, "BLUB", TextAttr.reset,"YYY");
##  "XXX\033[32mBLUB\033[0mYYY"
##  gap> str2 := WrapTextAttribute(str, TextAttr.1);
##  "\033[31mXXX\033[32mBLUB\033[0m\033[31m\027YYY\033[0m"
##  gap> str3 := WrapTextAttribute(str, TextAttr.underscore);
##  "\033[4mXXX\033[32mBLUB\033[0m\033[4m\027YYY\033[0m"
##  gap> # use Print(str); and so on to see how it looks like.
##  </Example>
##  </Description>
##  </ManSection>
##  
##  <#/GAPDoc>
InstallGlobalFunction(WrapTextAttribute, function(str, attr)
  if IsList(attr) and Length(attr) > 0 and IsString(attr[1]) and
     Length(attr[1]) > 1 and attr[1]{[1,2]} = TextAttr.CSI and
                            attr[2] = TextAttr.reset then
    attr := attr[1];
  fi;
  if IsString(attr) and Length(attr) > 1 and attr{[1,2]} = TextAttr.CSI then
    # we mark inner attribute starters by appending a char 23
    str := SubstitutionSublist(str, TextAttr.reset, Concatenation(
                                              TextAttr.reset, attr, "\027"));
    str := Concatenation(attr, str, TextAttr.reset);
  elif IsString(attr) then
    str := Concatenation(attr,str,attr);
  elif IsList(attr) and Length(attr) = 2 and IsString(attr[1]) and
    IsString(attr[2]) then
    str := Concatenation(attr[1], str, attr[2]);
  else
    Error("WrapTextAttribute: argument attr must be string or list of two strings.\n");
  fi;
  return str;
end);

##  <#GAPDoc Label="FormatParagraph">
##  <ManSection >
##  <Func Arg="str[, len][, flush][, attr][, widthfun]" 
##      Name="FormatParagraph" />
##  <Returns>the formatted paragraph as string</Returns>
##  <Description>
##  This function formats a text given  in the string <A>str</A> as a
##  paragraph. The optional arguments have the following meaning:
##  
##  <List >
##  <Mark><A>len</A></Mark>
##  <Item>the length of  the lines of the formatted  text, default is
##  <C>78</C> (counted without a visible length of the strings
##  specified in the <A>attr</A> argument)</Item>
##  <Mark><A>flush</A></Mark>
##  <Item>can  be <C>"left"</C>,  <C>"right"</C>, <C>"center"</C>  or
##  <C>"both"</C>, telling that lines should be flushed left, flushed
##  right, centered or left-right justified, respectively, default is
##  <C>"both"</C></Item>
##  <Mark><A>attr</A></Mark>
##  <Item>is a  list of two strings;  the first is prepended  and the
##  second  appended to  each line  of  the result  (can for  example
##  be  used  for  indenting,  <C>["  ",  ""]</C>,  or  some  markup,
##  <C>[TextAttr.bold,   TextAttr.reset]</C>,   default  is   <C>["",
##  ""]</C>)</Item>
##  <Mark><A>widthfun</A></Mark>
##  <Item>must be a function which returns the display width of text in 
##  <A>str</A>. The default is <C>Length</C> assuming that each byte 
##  corresponds to a character of width one. If <A>str</A> is given in 
##  <C>UTF-8</C> encoding one can use <Ref Func="WidthUTF8String"/> here.
##  </Item>
##  </List>
##  
##  This function  tries to handle  markup with the  escape sequences
##  explained in <Ref Var="TextAttr"/> correctly.
##  
##  <Example>
##  gap> str := "One two three four five six seven eight nine ten eleven.";;
##  gap> Print(FormatParagraph(str, 25, "left", ["/* ", " */"]));           
##  /* One two three four five */
##  /* six seven eight nine ten */
##  /* eleven. */
##  </Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  
BindGlobal("SPACESTRINGS", [" "]);
# only relevant for HPCGAP, to allow for formatting of help pages in any thread
if IsBound(HPCGAP) then
  MakeThreadLocal("SPACESTRINGS");                                             
fi;                                                                            

InstallGlobalFunction(FormatParagraph, function(arg)
  local   str,  len,  flush,  attr, width, i, words, esc, l, j, k, lw,  
          lines,  s,  ss,  nsp,  res,  a,  new,  qr,  b;
  str := arg[1];
  # default line length
  len := 78;
  # default flush (flush left and right)
  flush := "both";
  # default attribute (empty)
  attr := false;
  # default width function assumes that one byte is one character
  width := Length;
  # scan further arg's
  for i in [2..Length(arg)] do
    if IsInt(arg[i]) then
      len := arg[i];
    elif arg[i] in ["both", "left", "right", "center"] then
      flush := arg[i];
    elif IsList(arg[i]) then
      attr := arg[i];
    elif IsFunction(arg[i]) then
      width := arg[i];
    else
      Error("wrong argument", arg[i]);
    fi;
  od;
  for i in [Length(SPACESTRINGS)+1..len] do
    SPACESTRINGS[i] := Concatenation(SPACESTRINGS[i-1], " ");
  od;
  # we scan the string
  words := [];
  i := 1;
  esc := CHAR_INT(27);
  l := Length(str);
  while i<=l do
    if str[i] in WHITESPACE then
      # delete leading whitespace
      if Length(words)>0 then
        Add(words, 1);
      fi;
      i := i+1;
      while i<=l and str[i] in WHITESPACE do
        i := i+1;
      od;
    elif str[i] = esc then
      # sequences starting with ESC and stopping with the first letter
      # afterwards are not changed and considered to have length zero
      j := i+1;
      while j<=l and not str[j] in SMALLLETTERS and not str[j] in
        CAPITALLETTERS do
        j := j+1;
      od;
      if j>l then
        Error("string end inside escape sequence");
      else
        Add(words, [0, [i..j]]);
      fi;
      i := j+1;
    else
      j := i+1;
      while j<=l and not (str[j] in WHITESPACE or str[j]=esc) do
        j := j+1;
      od;
      if ForAll([i..j-1], k-> IsChar(str[k])) then
        Add(words, [width(str{[i..j-1]}), [i..j-1]]);
      else
        Add(words, [j-i, [i..j-1]]);
      fi;
      i := j;
    fi;
  od;
  # remove trailing white space
  lw := Length(words);
  if lw>0 and IsInt(words[lw]) then
    Unbind(words[lw]);
  fi;
  
  # split into lines
  lines := [];
  i := 1;
  lw := Length(words);
  while i <= lw do
    # make sure that at least first chunk without space is added to
    # next line (even, if too long)
    s := words[i][1];
    k := i+1;
    while k <= lw and IsList(words[k]) do
      s := s+words[k][1];
      k := k+1;
    od;
    j := k;
    nsp := 0;
    while j <= lw and s+nsp < len do
      if IsInt(words[j]) then
        nsp := nsp+1;
        j := j+1;
      else
        # line breaks only at white space
        ss := s+nsp;
        k := j;
        while k <= lw and IsList(words[k]) do
          ss := ss+words[k][1];
          k := k+1;
        od;
        if s=0 or ss <= len then
          s := ss-nsp;
          j := k;
        else
          break;
        fi;
      fi;
    od;
    if IsInt(words[j-1]) then
      Add(lines, [s,nsp-1,[i..j-2]]);
      i := j;
    else
      Add(lines, [s,nsp,[i..j-1]]);
      i := j+1;
    fi;
  od;
  
  # format lines
  res := "";
  for i in [1..Length(lines)] do
    a := lines[i];
    new := words{a[3]};
    # now fill with spaces
    nsp := len - a[1] - a[2];
    if nsp > 0 then
      if flush = "right" then
        new := Concatenation([nsp], new);
      elif flush = "both" and a[2] > 0 and i < Length(lines) then
        qr := QuotientRemainder(nsp, a[2]);
        for j in [1..Length(new)] do
          if IsInt(new[j]) then
            if qr[2]>0 then
              new[j] := new[j]+qr[1]+1;
              qr[2] := qr[2]-1;
            else
              new[j] := new[j]+qr[1];
            fi;
          fi;
        od;
      elif flush = "center" and nsp > 1 then
        new := Concatenation([QuoInt(nsp,2)], new);
      fi;
    fi;
    # add text attribute begin
    if attr <> false and Length(new)>0 then
      if IsInt(new[1]) then
        new := Concatenation([new[1]], [attr[1]], new{[2..Length(new)]});
      else
        Append(res, attr[1]);
      fi;
    fi;
    s := "";
    for b in new do
      if IsInt(b) then
        Append(s, SPACESTRINGS[b]);
      elif IsString(b) then
        Append(s, b);
      else # range
        Append(s, str{b[2]});
      fi;
    od;
    # add text attribute begin after each text attribute reset (if it
    # is an escape sequence) 
    # and the end attribute
    if attr <> false then
      if Length(attr[1])>2 and attr[1]{[1,2]} = TextAttr.CSI then
        s := SubstitutionSublist(s, TextAttr.reset, attr[1]);
      fi;
      Append(s, attr[2]);
    fi;
    Add(s, '\n');
    Append(res, s);
  od;
##  if PositionSublist(res,"\033[33X")<> fail and PositionSublist(res,"\033[133X")= fail then Error("FP"); fi;
##  if PositionSublist(res,"Emph")<> fail then Error("FP"); fi;
  return res;
end);

##  <#GAPDoc Label="StripEscapeSequences">
##  <ManSection >
##  <Func Arg="str" Name="StripEscapeSequences" />
##  <Returns>string without escape sequences</Returns>
##  <Description>
##  This  function  returns  the  string one  gets  from  the  string
##  <A>str</A> by  removing all escape sequences  which are explained
##  in <Ref Var="TextAttr"/>.  If <A>str</A> does not  contain such a
##  sequence then <A>str</A> itself is returned.
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  
InstallGlobalFunction(StripEscapeSequences, function(str)
  local   esc,  res,  i,  ls,  p;
  esc := CHAR_INT(27);
  res := "";
  i := 1;
  ls := Length(str);
  while i <= ls do
    if str[i] = esc then
      i := i+1;
      while not str[i] in LETTERS do
        i := i+1;
      od;
      # first letter is last character of escape sequence
      i := i+1; 
      # remove \027 marker of inner escape sequences as well
      if IsBound(str[i]) and str[i] = '\027' then
        i := i+1;
      fi;
    else
      p := Position(str, esc, i);
      if p=fail then
        if i=1 then
          # don't copy if no escape there
          return str;
        else
          Append(res, str{[i..ls]});
          return res;
        fi;
      else
        Append(res, str{[i..p-1]});
        i := p;
      fi;
    fi;
  od;
  return res;
end);
InstallGlobalFunction(SubstituteEscapeSequences, function(str, subs)
  local orig, special, hash, esc, res, i, ls, seq, pos, p, b, e, width, 
        cont, nb, ne, flush, pY, indlen, par, j, row, a, l, n, k, widthfun;

  # maybe we need to simplify some substitution strings because
  # of the current encoding, we cache the result
  if GAPInfo.TermEncoding <> "UTF-8" then
    if IsBound(subs.(GAPInfo.TermEncoding)) then
      subs := subs.(GAPInfo.TermEncoding);
    else
      orig := subs;
      subs := ShallowCopy(subs);
      for a in RecNames(subs) do
        if IsList(subs.(a)) then
          subs.(a) := [subs.(a)[1], List(subs.(a)[2], x-> 
                                Encode(
                                SimplifiedUnicodeString(Unicode(x, "UTF-8"), 
                                GAPInfo.TermEncoding),
                                GAPInfo.TermEncoding))];
        fi;
      od;
      orig.(GAPInfo.TermEncoding) := subs;
    fi;
  fi;
  # we need a special handling of tags to reformat paragraphs and to
  # fill lines
  special := [];
  for a in ["format", "FillString"] do
    Add(special, Position(subs.hash[1], subs.(a)[1][1]));
    Add(special, Position(subs.hash[1], subs.(a)[1][2]));
  od;
  hash := subs.hash;
  esc := CHAR_INT(27);
  res := "";
  i := 1;
  ls := Length(str);
  while i <= ls do
    if str[i] = esc and IsBound(str[i+1]) and str[i+1] = '[' then
      seq := "";
      i := i+1;
      while not str[i] in LETTERS do
        i := i+1;
        Add(seq, str[i]);
      od;
      # first letter is last character of escape sequence
      i := i+1; 
      if IsBound(str[i]) and str[i] = '\027' then
        cont := true;
        i := i+1;
      else
        cont := false;
      fi;
      pos := PositionSet(hash[1], seq);
      if pos <> fail and not pos in special then
        if cont and (Length(hash[2][pos]) = 0 or hash[2][pos][1] <> esc) then
          seq := "";
        else
          seq := hash[2][pos];
        fi;
      else
        Add(res, esc);
        Add(res, '[');
      fi;
      Append(res, seq);
    else
      p := Position(str, esc, i);
      if p=fail then
        if i=1 then
          # don't copy if no escape there
          return str;
        else
          Append(res, str{[i..ls]});
          i := ls+1;
        fi;
      else
        Append(res, str{[i..p-1]});
        i := p;
      fi;
    fi;
  od;
  # now we reformat paragraphs
  if GAPInfo.TermEncoding = "UTF-8" then
    widthfun := WidthUTF8String;
  else
    widthfun := Length;
  fi;
  str := res;
  res := "";
  pos := 0;
  b := Concatenation(TextAttr.CSI, subs.format[1][1]);
  e := Concatenation(TextAttr.CSI, subs.format[1][2]);
  width := SizeScreen()[1] - 2;
  while pos <> fail do
    nb := PositionSublist(str, b, pos);
    if nb = fail then
      Append(res, str{[pos+1..Length(str)]});
      pos := fail;
    else
      ne := PositionSublist(str, e, nb);
      # find flush mode
      if str[nb+Length(b)+2] = '0' then
        flush := subs.flush[2][1];
      elif str[nb+Length(b)+2] = '1' then
        flush := "left";
      elif str[nb+Length(b)+2] = '2' then
        flush := "center";
      else
        flush := "both";
      fi;
      Append(res, str{[pos+1..nb-1]});
      # find indent
      pY := Position(str, 'Y', nb);
      # the +2 because all help text has additional indentation of 2
      indlen := Int(str{[nb+Length(b)+4..pY-1]}) + 2;
      par := FormatParagraph(str{[pY+1..ne-1]}, width - indlen, flush,
                          [RepeatedString(" ", indlen), ""], widthfun);
      # remove leading blanks if there is already something on this line
      # (e.g., initial indentation or a list mark)
      i := Length(res);
      while i > 0 and res[i] <> '\n' do
        i := i-1;
      od;
      i := widthfun(StripEscapeSequences(res{[i+1..Length(res)]}));
      while i > 0 and par[1] = ' ' do
        Remove(par,1);
        i := i-1;
      od;
      if Length(par) > 1 and par[Length(par)] = '\n' then
        Unbind(par[Length(par)]);
      fi;
      Append(res, par);
      pos := ne + Length(e) - 1;
    fi;
  od;
  # a finally we expand the fill strings
  b := Concatenation(TextAttr.CSI, subs.FillString[1][1]);
  if PositionSublist(res, b) <> fail then
    str := res;
    res := "";
    pos := 0;
    nb := PositionSublist(str, b, pos);
    while nb <> fail do
      # find row
      i := nb-1;
      while i>0 and str[i] <> '\n' do
        i := i-1;
      od;
      j := nb+1;
      while Length(str) >= j and str[j] <> '\n' do
        j := j+1;
      od;
      Append(res, str{[pos+1..i]});
      # split row into pieces to fill
      row := [str{[i+1..nb-1]}, str{[nb+Length(b)..j-1]}];
      nb := PositionSublist(row[Length(row)], b);
      while nb <> fail do
        a := row[Length(row)];
        row[Length(row)] := a{[1..nb-1]};
        Add(row, a{[nb+Length(b)..Length(a)]});
        nb := PositionSublist(row[Length(row)], b);
      od;
      # lengths of the fillings
      l := width - Sum(row, a-> widthfun(StripEscapeSequences(a)));
      n := Length(row)-1;
      ls := [];
      for k in [1..n] do
        Add(ls, QuoInt(l,n));
      od;
      k := n;
      while Sum(ls) < l do
        ls[k] := ls[k]+1;
        k := k-1;
      od;
      # cannot do much when line already too long
      if l < 0 then
        ls := 0*ls;
      fi;
      for i in [1..n] do
        Append(res, row[i]);
        Append(res, RepeatedUTF8String(subs.FillString[2][1], ls[i]));
      od;
      Append(res, row[n+1]);
      pos := j-1;
      nb := PositionSublist(str, b, pos);
    od;
    Append(res, str{[pos+1..Length(str)]});
  fi;
  return res;
end);



##  <#GAPDoc Label="WordsString">
##  <ManSection >
##  <Func Arg="str" Name="WordsString" />
##  <Returns>list of strings containing the words</Returns>
##  <Description>
##  This returns  the list of  words of a  text stored in  the string
##  <A>str</A>. All non-letters are considered as word boundaries and
##  are removed.
##  <Example>
##  gap> WordsString("one_two \n    three!?");
##  [ "one", "two", "three" ]
##  </Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  
InstallGlobalFunction(WordsString, function(str)
  local   nonletters, wds;
  nonletters := Set("0123456789 \n\r\t\b+*~^\\\"#'`'/?-_.:,;<>|=()[]{}&%$§!");
  wds := SplitString(str, "", nonletters);
  return wds;
end);

# The GAP library will contain a new function CrcString. To make GAPDoc
# running with current/older versions of GAP we use the following helper,
# if necessary with a simple fallback.
if IsBoundGlobal("CrcString") then
  InstallGlobalFunction(CrcText, CrcString); 
else
  InstallGlobalFunction(CrcText, function(s)
    local n, res;
    n := "guckCRCXQWYNVOH";
    FileString(n, s);
    res := CrcFile(n);
    RemoveFile(n);
    return res;
  end);
fi;

##  <#GAPDoc Label="Base64String">
##  <ManSection >
##  <Func Arg="str" Name="Base64String" />
##  <Func Arg="bstr" Name="StringBase64" />
##  <Returns>a string</Returns>
##  <Description>
##  The  first  function  translates  arbitrary   binary  data  given  as  a
##  GAP  string  into   a  <E>base  64</E>  encoded   string.  This  encoded
##  string  contains  only  printable  ASCII   characters  and  is  used  in
##  various  data  transfer  protocols  (<C>MIME</C>  encoded  emails,  weak
##  password   encryption,  ...).   We   use  the   specification  in   <URL
##  Text="RFC&#160;2045">https://www.rfc-editor.org/rfc/rfc2045</URL>.<P/>
##  
##  The second function  has the reverse functionality. Here  we also accept
##  the characters  <C>-_</C> instead of  <C>+/</C> as last  two 
##  characters.  Whitespace is ignored.
##  
##  <Example>
##  gap> b := Base64String("This is a secret!");
##  "VGhpcyBpcyBhIHNlY3JldCEA="
##  gap> StringBase64(b);                       
##  "This is a secret!"
##  </Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
BindGlobal("Base64LETTERS",
          "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
BindGlobal("Base64REVERSE",
[,,,,,,,,,-1,,,-1,,,,,,,,,,,,,,,,,,,-1,,,,,,,,,,,62,,62,,63,52,53,54,55,56,57,58,59,60,61,,,,-2,,,,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,,,,,63,,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51]);

MakeImmutable(Base64LETTERS);
MakeImmutable(Base64REVERSE);

InstallGlobalFunction(Base64String, function(str)
  local istr, pad, i, res, a, d, c, b;
  istr := INTLIST_STRING(str, 1);
  pad := (-Length(istr)) mod 3;
  for i in [1..pad] do
    Add(istr,0);
  od;
  i := 1;
  res := "";
  while i < Length(istr) do
    if i > 1 and i mod 57 = 1 then
      Add(res, '\n');
    fi;
    a := (istr[i]*256+istr[i+1])*256+istr[i+2];
    d := RemInt(a, 64);
    a := (a-d)/64;
    c := RemInt(a, 64);
    a := (a-c)/64;
    b := RemInt(a, 64);
    a := (a-b)/64;
    Append(res, Base64LETTERS{[a,b,c,d]+1});
    i := i+3;
  od;
  if i mod 57 = 1 and pad > 0 then
    Add(res, '\n');
  fi;
  for i in [1..pad] do
    Add(res, '=');
  od;
  return res;
end);
 
InstallGlobalFunction(StringBase64, function(bstr)
  local istr, res, j, n, d, c, a;
  istr := Base64REVERSE{INTLIST_STRING(bstr, 1)};
  res := [];
  j := 0;
  n := 0;
  for a in istr do
    if a <> -1 then
      if a = -2 then
        Unbind(res[Length(res)]);
      else
        n := n*64+a;
        j := j+1;
        if j = 4 then
          d := RemInt(n, 256);
          n := (n-d)/256;
          c := RemInt(n, 256);
          n := (n-c)/256;
          Append(res, [n,c,d]);
          j := 0;
          n := 0;
        fi;
      fi;
    fi;
  od;
  return STRING_SINTLIST(res);
end);


