% File:          latex.sl    -*- mode: SLang; mode: fold -*-
%
% Copyright (c)
%       2007         Jörg Sommer <[email protected]>
%       $Id: latex_typo.sl 206 2007-09-08 13:38:39Z joerg $
%
%       -*- This file is part of Jörg's LaTeX Mode (JLM) -*-
%
% Description: This file includes all functions related to typography.
%
% License: This program is free software; you can redistribute it and/or
%          modify it under the terms of the GNU General Public License as
%          published by the Free Software Foundation; either version 2 of
%          the License, or (at your option) any later version.
%
%          This program is distributed in the hope that it will be
%          useful, but WITHOUT ANY WARRANTY; without even the implied
%          warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
%          PURPOSE.  See the GNU General Public License for more details.

if (current_namespace() != "latex")
 throw UsageError, "You must load this file into the namespace \"latex\"";

private define is_active()
{
   return andelse {LaTeX_Typo_Active} {not is_verbatim()};
}

private variable b_n_c_del;
private variable b_n_c_insert;
private variable b_n_c_fun;

private define before_next_char_hook(fun);
private define before_next_char_hook(fun)
{
   debug_msg(sprintf("%s: %c, %S, %s, %s, %S", _function_name(), LAST_CHAR,
                     fun, b_n_c_del, b_n_c_insert, b_n_c_fun) );

   EXIT_BLOCK
   {
       remove_from_hook("_jed_before_key_hooks", &before_next_char_hook());
       __uninitialize(&b_n_c_del);
   }

   if (andelse {typeof(fun) == String_Type}
       {orelse {fun == "self_insert_cmd"} {strncmp(fun, "latex->", 7) == 0}}
       {@b_n_c_fun(LAST_CHAR)} )
   {
       variable len = strlen(b_n_c_del);
       () = left(len);
       if ( looking_at(b_n_c_del) )
         () = replace_chars(len, b_n_c_insert);
       else
         () = right(len);
   }
}

private define enable_before_next_char_hook(del, ins, fun)
{
   if ( __is_initialized(&b_n_c_del) )
     throw UsageError, "before_next_char_hook already active";

   b_n_c_del = del;
   b_n_c_insert = ins;
   b_n_c_fun = fun;

   add_to_hook("_jed_before_key_hooks", &before_next_char_hook());
}

private define is_alpha_or_hyphen(char)
{
   return orelse {char == '-'} {isalpha(char)};
}

%!%+
%\function{typo_slash()}
%\synopsis{Replaces a / after a word by \\slash{} or adds ""}
%\usage{typo_slash()}
%\description
%  LaTeX does not break words combined with / after the slash, e.g. the
%  text "input/output" is one block and is not wrapped before output.
%
%  If the number of characters before the / is at least \var{LaTeX_Typo_Word_Size},
%  the / is replaced by \\slash{}, if the package babel is not loaded, or a
%  "" is appended, if the package babel is loaded. \\slash{} is a / with an
%  hyphenpoint after it. Unfortunely, it removes all other hyphenpoints
%  from word after the slash. This does not happen with "" from babel.
%
%\notes
%  You can disable this function by setting the variable \var{LaTeX_Typo_Active}
%  to 0. This still inserts the / character as \sfun{self_insert_cmd} would do.
%
%\seealso{LaTeX_Typo_Active, LaTeX_Typo_Word_Size}
%!%-
static define typo_slash()
{
   if (LAST_CHAR != '/')
     throw UsageError, "typo_slash called for anything else than /";

   % Insert the / and do abbrev expansion
   call("self_insert_cmd");

   !if ( is_active() )
     return;

   push_spot();
   EXIT_BLOCK
   {
       pop_spot();
   }

   () = left(1);
   if ( blooking_at("-") or blooking_at("\"~") or blooking_at("\"\"") )
   {
       EXIT_BLOCK {};
       pop_spot();

       if ( pkg_loaded("babel") )
         insert("\"\"");
       else
         () = replace_chars(1, "\\slash{}");

       return;
   }

   _get_point();
   bskip_word_chars();
   if ( () - _get_point() < LaTeX_Typo_Word_Size )
     return;

   if ( pkg_loaded("babel") )
     enable_before_next_char_hook("", "\"\"", &is_alpha_or_hyphen());
   else
     enable_before_next_char_hook("/", "\\slash{}", &isalpha());
}

%!%+
%\function{typo_percent()}
%\synopsis{Inserts a \\, before \\%}
%\usage{typo_percent()}
%\description
%  In german typography, a spatium (\\,) should be before a percent sign.
%  This function removes all whitespaces before \\% and insert an \\,.
%
%\notes
%  You can disable this function by setting the variable \var{LaTeX_Typo_Active}
%  to 0. This still inserts the % character as \sfun{self_insert_cmd} would do.
%
%\seealso{LaTeX_Typo_Active}
%!%-
static define typo_percent()
{
   if (LAST_CHAR != '%')
     throw UsageError, "typo_percent called for anything else than %";

   if (andelse {is_active()} {is_escaped()})
   {
       () = left(1);
       trim();
       !if ( blooking_at("\\,") )
         insert("\\,");
       () = right(1);
   }

   call("self_insert_cmd");
}

%!%+
%\function{typo_abbrev()}
%\synopsis{Inserts a \\, between abbreviations}
%\usage{typo_abbrev()}
%\description
%  In german typography, a spatium (\\,) should be after the dot inside of
%  an abbreviation, e.g. z.\\,B. or i.\\,d.\\,R. This functions checks if the
%  number of characters before the first dot and before the current point is
%  less than LaTeX_Typo_Word_Size and inserts a \\, after the previous dot,
%  if it is so.
%
%\notes
%  You can disable this function by setting the variable \var{LaTeX_Typo_Active}
%  to 0. This still inserts the % character as \sfun{self_insert_cmd} would do.
%
%\seealso{LaTeX_Typo_Active, LaTeX_Typo_Word_Size}
%!%-
static define typo_abbrev()
{
   if (LAST_CHAR != '.')
     throw UsageError, "typo_abbrev called for anything else than .";

   call("self_insert_cmd");

   if ( is_active() )
   {
       push_spot();
       EXIT_BLOCK
       {
           pop_spot();
       }
       () = left(1);

       _get_point();
       bskip_word_chars();
       variable len = () - _get_point();
       if (len == 0 or len >= LaTeX_Typo_Word_Size)
         return;
       bskip_white();

       () = left(1);
       !if ( looking_at_char('.') )
         return;

       push_spot();
       EXIT_BLOCK
       {
           pop_spot();
           pop_spot();
       }

       _get_point();
       bskip_word_chars();
       if ( () - _get_point() >= LaTeX_Typo_Word_Size )
         return;

       pop_spot();
       () = right(1);
       trim();
       insert("\\,");
   }
}

%!%+
%\function{typo_german_decimal_point()}
%\synopsis{Puts a comma between digits into \\mathord{}}
%\usage{typo_german_decimal_point()}
%\description
%  In german, the comma is the separator of the fractional part of
%  numbers. In LaTeX, the default meaning of , in math mode is as a list
%  separator. Due to this it's necessary to tell LaTeX that the , is the
%  decimal point.
%
%  This functions checks, if the characters before the comma are digits
%  with a leading space and if the characters behind the comma are digits.
%  Then it replaces the comma by \\mathord{,}. In text mode, this functions
%  does nothing special.
%
%\notes
%  You can disable this function by setting the variable \var{LaTeX_Typo_Active}
%  to 0. This still inserts the , character as \sfun{self_insert_cmd} would do.
%
%\seealso{LaTeX_Typo_Active}
%!%-
static define typo_german_decimal_point()
{
   if (LAST_CHAR != ',')
     throw UsageError, "typo_german_decimal_point called for anything else than ,";

   call("self_insert_cmd");

   !if (andelse {is_active()} {is_math()})
     return;

   push_spot();
   EXIT_BLOCK
   {
       pop_spot();
   }

   () = left(1);
   _get_point();
   bskip_chars("0-9");
   if ( andelse {() - _get_point() > 0}
        {orelse {bolp()} {left(1) and is_substr(" \t$", char(what_char()))}} )
     enable_before_next_char_hook(",", "\\mathord{,}", &isdigit());
}

private variable word_char_count;

private define hyphen_hook(fun);
private define hyphen_hook(fun)
{
   debug_msg(_function_name() + ": " + char(LAST_CHAR) + ", $fun"$);

   EXIT_BLOCK
   {
       remove_from_hook("_jed_before_key_hooks", &hyphen_hook());
       __uninitialize(&word_char_count);
   }

   variable replace_str, back = 1;

   if (andelse {typeof(fun) == String_Type} {fun == "self_insert_cmd"}
         {isalpha(LAST_CHAR)})
   {
       if ( __is_initialized(&word_char_count) )
         % 8.
       {
           debug_msg("hyphen_hook: $word_char_count"$);
           ++word_char_count;
           if (word_char_count < LaTeX_Typo_Word_Size)
           {
               EXIT_BLOCK;
               return;
           }

           back = word_char_count;
           replace_str = "\"=";
       }
       else
         % 7.
         replace_str = "\"~";
   }
   else if (LAST_CHAR == ')')
     % 6.
   {
       replace_str = "\"~";
       enable_before_next_char_hook("", "\"\"", &isalpha());
   }
   else if (LAST_CHAR == '/')
     % 5.
     replace_str = "\"~";
   else
     return;

   () = left(back);
   () = replace_chars(1, replace_str);
   () = right(back - 1);
}

%!%+
%\function{typo_hyphen()}
%\synopsis{Replace a hyphen by a special hyphen}
%\usage{typo_hyphen()}
%\description
%  A simple - is not everywhere what you want. Babel defines some special
%  hypens:
%#v+
%  texdoc gerdoc section 2.2.4 or texdoc babel
%    ""    an unvisible hyphen point
%    "~    an hyphen without an hyphen point
%    "=    an hyphen that doesn't prevents hyphenation in the
%          rest of the word lik - does
%#v-
%
% \sfun{typo_hyphen} makes the following replacements:
%#v+
%  w = word character     s = space ( \t)
%  W = at least LaTeX_Typo_Word_Size word chars
%
%   rule               |     example
%  --------------------+-------------------
%   1. $-w -> $"~w     | $i$-mal, $\alpha$-Teilchen
%   2. }-w -> }"~w     | \LaTeX{}-Aufruf
%   3. \w-w -> \w"~w   | \LaTeX-Aufruf
%   4. s-w -> s"~w     | bergauf und -ab
%   5. w-/ -> w"~/     | Ein-/Ausgabe
%   6. w-) -> w"~)     | (Haupt-)Aufgabe
%   7. w-w -> w"~w     | I-Punkt, HI-Virus
%   8. W-W -> W"=W     | primitiv-rekursiv
%   9. /-w -> /"~w     | Vorlesungsgliederung/-struktur
%  10. /""-w -> /"""~w | Vorlesungsgliederung/-struktur
%#v-
%\notes
%  You can disable this function by setting the variable \var{LaTeX_Typo_Active}
%  to 0. This still inserts the - character as \sfun{self_insert_cmd} would do.
%
%\seealso{LaTeX_Typo_Active}
%!%-
static define typo_hyphen()
{
   if (LAST_CHAR != '-')
     throw UsageError, "typo_hyphen called for anything else than -";

   call("self_insert_cmd");

   !if (andelse {is_active()} {pkg_loaded("babel")})
     return;

   push_spot();
   EXIT_BLOCK
   {
       pop_spot();
   }

   if ( orelse {left(2) and is_substr("$}/ \t", char(what_char()) )}
        {left(2) and looking_at("/\"\"")} )
     % 1. + 2. + 4. + 9. + 10.
   {
       enable_before_next_char_hook("-", "\"~", &isalpha());
       return;
   }

   () = right(3);
   _get_point();
   bskip_word_chars();
   variable char_cnt = () - _get_point();

   if (char_cnt == 0)
     return;

   if ( is_escaped() )
     % 3.
     enable_before_next_char_hook("-", "\"~", &isalpha());
   else
     % 5.--8.
   {
       if (char_cnt >= LaTeX_Typo_Word_Size)
         % 8.
         word_char_count = 0;

       add_to_hook("_jed_before_key_hooks", &hyphen_hook());
   }
}

static define typo_dots()
{
   if ( orelse {is_verbatim()} {not blooking_at("..")} )
   {
       typo_abbrev();
       return;
   }

   () = left(2);
   push_spot();

   variable text;
   !if ( is_math() )
   {
       push_mark();
       bskip_chars(" ");
       () = left(1);
       !if ( looking_at_char(',') )
         () = right(1);
       text = bufsubstr();

       pop_spot();
       deln(2);
       try
         cmd_insert("dots", 0);
       catch ApplicationError:
         cmd_insert("ldots");

       insert(text);
       return;
   }

   bskip_chars(" ");
   () = left(1);

   variable ch = what_char();
   switch (ch)
   { case ',':
       if ( pkg_loaded("amsmath") )
          text = "\\dotsc,";
       else
         text = "\\ldots,";
   }
   { case '+' or case '-' or case '<' or case '>' or case '=':
       if ( pkg_loaded("amsmath") )
         text = "\\dotsb" + char(what_char());
       else
         text = "\\cdots" + char( what_char() );
   }
   { case '}':
       if ( pkg_loaded("amsmath") )
         text = "\\dotso";
       else
         text = "\\ldots";
   }
   {
       variable cmd = is_command();

       switch(cmd)
       { case "\\leq" or case "\\geq" or case "\\pm" or case "\\mp" or
              case "\\cup" or case "\\cap" or case "\\vee" or case "\\wedge":
           if ( pkg_loaded("amsmath") )
             text = "\\dotsb" + cmd;
           else
             text = "\\cdots" + cmd;
       }
       { case "\\int" or case "\\sum" or case "\\prod":
           if ( pkg_loaded("amsmath") )
             text = "\\dotsi";
           else
             text = "\\cdots";

           pop_spot();
           () = replace_chars(2, text+cmd);
           return;
       }
       {
           if ( pkg_loaded("amsmath") )
             text = "\\dotso ";
           else
             text = "\\ldots ";

           pop_spot();
           () = replace_chars(2, text);
           return;
       }
   }
   bskip_chars(" ");
   () = left(1);

   variable looping = 0;
   while ( looking_at_char('}') )
   {
       variable mark = create_user_mark();
       if (find_matching_delimiter('}') != 1)
       {
           goto_user_mark(mark);
           break;
       }
       () = left(1);
       switch ( what_char() )
       { case '_' or case '^':
           if (looping == 0)
           {
               () = right(2);
               push_mark();
               () = left(3);
           }
           else
             () = left(1);

           ++looping;
       }
       {
           goto_user_mark(mark);
           break;
       }
   }

   if (looping == 0)
   {
       pop_spot();
       () = replace_chars(2, text);
       return;
   }

   if ( re_looking_at("[0-9]") )
     bskip_chars("0-9");
   else
   {
       variable found_brakets = 0;
       if ( andelse {looking_at_char('}')} {find_matching_delimiter('}') == 1} )
       {
           found_brakets = 1;
           () = left(1);
       }

       cmd = is_command();
       switch (cmd)
       { case "\\int" or case "\\sum" or case "\\prod":
           if ( pkg_loaded("amsmath") )
             text = "\\dotsi";
           else
             text = "\\cdots";

           pop_mark(0);
           pop_spot();
           () = replace_chars(2, text+cmd);
           return;
       }
       { case NULL:
           if (found_brakets)
             () = right(1);
           else
           {
               if ( re_looking_at("[A-Za-z]") )
                 bskip_chars("A-Za-z");
               text += " ";
           }
       }
   }
   text += bufsubstr();

   pop_spot();
   () = replace_chars(2, text+"}");
   () = left(1);
}