% \section{Included Python scripts}
% \label{sec:included-scripts}
%
% Here we describe the Python code for |run-sagetex-if-necessary|, and
% also |makestatic.py|, which removes \ST commands to produce a
% ``static'' file, and |extractsagecode.py|, which extracts all the Sage
% code from a |.tex| file.
%
% \subsection{run-sagetex-if-necessary}
% \label{sec:run-sagetex-if-necessary}
% \iffalse
%<*ifnecessaryscript>
% \fi
%
% When working on a document that uses \ST, running Sage every time you
% typeset your document may take too long, especially since it often
% won't be necessary. This script is a drop-in replacement for Sage:
% instead of
% \begin{center}
% |sage document.sagetex.sage|
% \end{center}
% you can do
% \begin{center}
% |run-sagetex-if-necessary.py document.sagetex.sage|
% \end{center}
% and it will use the MD5 mechanism described in the |endofdoc| macro
% (page~{\pageref{macro:endofdoc}). With this, you can set up your editor
% (\TeX Shop, \TeX Works, etc) to typeset your document with a script
% that does
% \begin{quote}
%   |pdflatex $1|\\
%   |run-sagetex-if-necessary.py $1|
% \end{quote}
% which will only, of course, run Sage when necessary.
%    \begin{macrocode}

# given a filename f, examines f.sagetex.sage and f.sagetex.sout and
# runs Sage if necessary.

import hashlib
import sys
import os
import re
import subprocess
from six import PY3

# CHANGE THIS AS APPROPRIATE
path_to_sage = os.path.expanduser('~/bin/sage')
# or try to auto-find it:
# path_to_sage = subprocess.check_output(['which', 'sage']).strip()
# or just tell me:
# path_to_sage = '/usr/local/bin/sage'

if sys.argv[1].endswith('.sagetex.sage'):
    src = sys.argv[1][:-13]
else:
    src = os.path.splitext(sys.argv[1])[0]

usepackage = r'\usepackage(\[.*\])?{sagetex}'
uses_sagetex = False

# if it doesn't use sagetex, obviously running sage is unnecessary
with open(src + '.tex') as texf:
    for line in texf:
        if re.search(usepackage, line.replace(r'\%', '').split('%')[0]):
            uses_sagetex = True
            break

if not uses_sagetex:
    print(src + ".tex doesn't seem to use SageTeX, exiting.")
    sys.exit(0)

# if something goes wrong, assume we need to run Sage
run_sage = True
ignore = r"^( _st_.goboom|print\('SageT| ?_st_.current_tex_line)"

try:
    with open(src + '.sagetex.sage', 'r') as sagef:
        h = hashlib.md5()
        for line in sagef:
            if not re.search(ignore, line):
                if PY3:
                    h.update(bytearray(line,'utf8'))
                else:
                    h.update(bytearray(line))
except IOError:
    print('{0}.sagetex.sage not found, I think you need to typeset {0}.tex first.'.format(src))
    sys.exit(1)

try:
    with open(src + '.sagetex.sout', 'r') as outf:
        for line in outf:
            m = re.match('%([0-9a-f]+)% md5sum', line)
            if m:
                print('computed md5:', h.hexdigest())
                print('sagetex.sout md5:', m.group(1))
                if h.hexdigest() == m.group(1):
                    run_sage = False
                    break
except IOError:
    pass

if run_sage:
    print('Need to run Sage on {0}.'.format(src))
    sys.exit(subprocess.call([path_to_sage, src + '.sagetex.sage']))
else:
    print('Not necessary to run Sage on {0}.'.format(src))
%    \end{macrocode}
%
% \subsection{makestatic.py}
% \label{sec:makestatic}
% \iffalse
%</ifnecessaryscript>
%<*staticscript>
% \fi
%
% Now the |makestatic.py| script. It's about the most basic, generic
% Python script taking command-line arguments that you'll find. The
% |#!/usr/bin/env python| line is provided for us by the |.ins| file's
% preamble, so we don't put it here.
%    \begin{macrocode}
import sys
import time
import getopt
import os.path
from sagetexparse import DeSageTex

def usage():
  print("""Usage: %s [-h|--help] [-o|--overwrite] inputfile [outputfile]

Removes SageTeX macros from `inputfile' and replaces them with the
Sage-computed results to make a "static" file. You'll need to have run
Sage on `inputfile' already.

`inputfile' can include the .tex extension or not. If you provide
`outputfile', the results will be written to a file of that name.
Specify `-o' or `--overwrite' to overwrite the file if it exists.

See the SageTeX documentation for more details.""" % sys.argv[0])

try:
  opts, args = getopt.getopt(sys.argv[1:], 'ho', ['help', 'overwrite'])
except getopt.GetoptError, err:
  print(str(err))
  usage()
  sys.exit(2)

overwrite = False
for o, a in opts:
  if o in ('-h', '--help'):
    usage()
    sys.exit()
  elif o in ('-o', '--overwrite'):
    overwrite = True

if len(args) == 0 or len(args) > 2:
  print('Error: wrong number of arguments. Make sure to specify options first.\n')
  usage()
  sys.exit(2)

if len(args) == 2 and (os.path.exists(args[1]) and not overwrite):
  print('Error: %s exists and overwrite option not specified.' % args[1])
  sys.exit(1)

src, ext = os.path.splitext(args[0])
%    \end{macrocode}
% All the real work gets done in the line below. Sorry it's not more
% exciting-looking.
%    \begin{macrocode}
desagetexed = DeSageTex(src)
%    \end{macrocode}
% This part is cool: we need double percent signs at the beginning of
% the line because Python needs them (so they get turned into single
% percent signs) \emph{and} because \textsf{Docstrip} needs them (so the
% line gets passed into the generated file). It's perfect!
%    \begin{macrocode}
header = "%% SageTeX commands have been automatically removed from this file and\n%% replaced with plain LaTeX. Processed %s.\n" % time.strftime('%a %d %b %Y %H:%M:%S', time.localtime())

if len(args) == 2:
  dest = open(args[1], 'w')
else:
  dest = sys.stdout

dest.write(header)
dest.write(desagetexed.result)
%    \end{macrocode}
%
% \iffalse
%</staticscript>
%<*extractscript>
% \fi
%
% \subsection{extractsagecode.py}
%
% Same idea as |makestatic.py|, except this does basically the opposite
% thing.
%    \begin{macrocode}
import sys
import time
import getopt
import os.path
from sagetexparse import SageCodeExtractor

def usage():
  print("""Usage: %s [-h|--help] [-o|--overwrite] inputfile [outputfile]

Extracts Sage code from `inputfile'.

`inputfile' can include the .tex extension or not. If you provide
`outputfile', the results will be written to a file of that name,
otherwise the result will be printed to stdout.

Specify `-o' or `--overwrite' to overwrite the file if it exists.

See the SageTeX documentation for more details.""" % sys.argv[0])

try:
  opts, args = getopt.getopt(sys.argv[1:], 'ho', ['help', 'overwrite'])
except getopt.GetoptError, err:
  print(str(err))
  usage()
  sys.exit(2)

overwrite = False
for o, a in opts:
  if o in ('-h', '--help'):
    usage()
    sys.exit()
  elif o in ('-o', '--overwrite'):
    overwrite = True

if len(args) == 0 or len(args) > 2:
  print('Error: wrong number of arguments. Make sure to specify options first.\n')
  usage()
  sys.exit(2)

if len(args) == 2 and (os.path.exists(args[1]) and not overwrite):
  print('Error: %s exists and overwrite option not specified.' % args[1])
  sys.exit(1)

src, ext = os.path.splitext(args[0])
sagecode = SageCodeExtractor(src)
header = """\
# This file contains Sage code extracted from %s%s.
# Processed %s.

""" % (src, ext, time.strftime('%a %d %b %Y %H:%M:%S', time.localtime()))

if len(args) == 2:
  dest = open(args[1], 'w')
else:
  dest = sys.stdout

dest.write(header)
dest.write(sagecode.result)
%    \end{macrocode}
%
% \iffalse
%</extractscript>
%<*parsermod>
% \fi
%
% \subsection{The parser module}
% \changes{v2.2}{2009/06/17}{Update parser module to handle pause/unpause}
%
% Here's the module that does the actual parsing and replacing. It's
% really quite simple, thanks to the awesome
% \href{http://pyparsing.wikispaces.com}{Pyparsing module}. The parsing
% code below is nearly self-documenting! Compare that to fancy regular
% expressions, which sometimes look like someone sneezed punctuation all
% over the screen.
%    \begin{macrocode}
import sys
from pyparsing import *
%    \end{macrocode}
% First, we define this very helpful parser: it finds the matching
% bracket, and doesn't parse any of the intervening text. It's basically
% like hitting the percent sign in Vim. This is useful for parsing \LTX
% stuff, when you want to just grab everything enclosed by matching
% brackets.
%    \begin{macrocode}
def skipToMatching(opener, closer):
  nest = nestedExpr(opener, closer)
  nest.setParseAction(lambda l, s, t: l[s:getTokensEndLoc()])
  return nest

curlybrackets = skipToMatching('{', '}')
squarebrackets = skipToMatching('[', ']')
%    \end{macrocode}
% Next, parser for |\sage|, |\sageplot|, and pause/unpause calls:
%    \begin{macrocode}
sagemacroparser = r'\sage' + curlybrackets('code')
sageplotparser = (r'\sageplot'
                 + Optional(squarebrackets)('opts')
                 + Optional(squarebrackets)('format')
                 + curlybrackets('code'))
sagetexpause = Literal(r'\sagetexpause')
sagetexunpause = Literal(r'\sagetexunpause')
%    \end{macrocode}
%
% With those defined, let's move on to our classes.
%
% \begin{macro}{SoutParser}
% Here's the parser for the generated |.sout| file. The code below does
% all the parsing of the |.sout| file and puts the results into a
% list. Notice that it's on the order of 10 lines of code---hooray
% for Pyparsing!
%    \begin{macrocode}
class SoutParser():
  def __init__(self, fn):
    self.label = []
%    \end{macrocode}
% A label line looks like
% \begin{quote}
%  |\newlabel{@sageinline|\meta{integer}|}{|\marg{bunch of \LTX code}|{}{}{}{}}|
% \end{quote}
% which makes the parser definition below pretty obvious. We assign some
% names to the interesting bits so the |newlabel| method can make the
% \meta{integer} and \meta{bunch of \LTX code} into the keys and values
% of a dictionary. The |DeSageTeX| class then uses that dictionary to
% replace bits in the |.tex| file with their Sage-computed results.
%    \begin{macrocode}
    parselabel = (r'\newlabel{@sageinline'
                 + Word(nums)('num')
                 + '}{'
                 + curlybrackets('result')
                 + '{}{}{}{}}')
%    \end{macrocode}
% We tell it to ignore comments, and hook up the list-making method.
%    \begin{macrocode}
    parselabel.ignore('%' + restOfLine)
    parselabel.setParseAction(self.newlabel)
%    \end{macrocode}
% A |.sout| file consists of one or more such lines. Now go parse the
% file we were given.
%    \begin{macrocode}
    try:
      OneOrMore(parselabel).parseFile(fn)
    except IOError:
      print('Error accessing {}; exiting. Does your .sout file exist?'.format(fn))
      sys.exit(1)
%    \end{macrocode}
% Pyparser's parse actions get called with three arguments: the string
% that matched, the location of the beginning, and the resulting parse
% object. Here we just add a new key-value pair to the dictionary,
% remembering to strip off the enclosing brackets from the ``result''
% bit.
%    \begin{macrocode}
  def newlabel(self, s, l, t):
    self.label.append(t.result[1:-1])
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{DeSageTeX}
% Now we define a parser for \LTX files that use \ST commands. We assume
% that the provided |fn| is just a basename.
%    \begin{macrocode}
class DeSageTex():
  def __init__(self, fn):
    self.sagen = 0
    self.plotn = 0
    self.fn = fn
    self.sout = SoutParser(fn + '.sagetex.sout')
%    \end{macrocode}
% Parse |\sage| macros. We just need to pull in the result from the
% |.sout| file and increment the counter---that's what |self.sage| does.
%    \begin{macrocode}
    smacro = sagemacroparser
    smacro.setParseAction(self.sage)
%    \end{macrocode}
% Parse the |\usepackage{sagetex}| line. Right now we don't support
% comma-separated lists of packages.
%    \begin{macrocode}
    usepackage = (r'\usepackage'
                 + Optional(squarebrackets)
                 + '{sagetex}')
    usepackage.setParseAction(replaceWith(r"""% "\usepackage{sagetex}" line was here:
\RequirePackage{verbatim}
\RequirePackage{graphicx}
\newcommand{\sagetexpause}{\relax}
\newcommand{\sagetexunpause}{\relax}"""))
%    \end{macrocode}
% Parse |\sageplot| macros.
%    \begin{macrocode}
    splot = sageplotparser
    splot.setParseAction(self.plot)
%    \end{macrocode}
% The printed environments (|sageblock| and |sageverbatim|) get turned
% into |verbatim| environments.
%    \begin{macrocode}
    beginorend = oneOf('begin end')
    blockorverb = 'sage' + oneOf('block verbatim')
    blockorverb.setParseAction(replaceWith('verbatim'))
    senv = '\\' + beginorend + '{' + blockorverb + '}'
%    \end{macrocode}
% The non-printed |sagesilent| environment gets commented out. We could
% remove all the text, but this works and makes going back to \ST
% commands (de-de-\ST{}ing?) easier.
%    \begin{macrocode}
    silent = Literal('sagesilent')
    silent.setParseAction(replaceWith('comment'))
    ssilent = '\\' + beginorend + '{' + silent + '}'
%    \end{macrocode}
% The |\sagetexindent| macro is no longer relevant, so remove it from
% the output (``suppress'', in Pyparsing terms).
%    \begin{macrocode}
    stexindent = Suppress(r'\setlength{\sagetexindent}' + curlybrackets)
%    \end{macrocode}
% Now we define the parser that actually goes through the file. It just
% looks for any one of the above bits, while ignoring anything that
% should be ignored.
%    \begin{macrocode}
    doit = smacro | senv | ssilent | usepackage | splot | stexindent
    doit.ignore('%' + restOfLine)
    doit.ignore(r'\begin{verbatim}' + SkipTo(r'\end{verbatim}'))
    doit.ignore(r'\begin{comment}' + SkipTo(r'\end{comment}'))
    doit.ignore(r'\sagetexpause' + SkipTo(r'\sagetexunpause'))
%    \end{macrocode}
% We can't use the |parseFile| method, because that expects a ``complete
% grammar'' in which everything falls into some piece of the parser.
% Instead we suck in the whole file as a single string, and run
% |transformString| on it, since that will just pick out the interesting
% bits and munge them according to the above definitions.
%    \begin{macrocode}
    str = ''.join(open(fn + '.tex', 'r').readlines())
    self.result = doit.transformString(str)
%    \end{macrocode}
% That's the end of the class constructor, and it's all we need to do
% here. You access the results of parsing via the |result| string.
%
% We do have two methods to define. The first does the same thing that
% |\ref| does in your \LTX file: returns the content of the label and
% increments a counter.
%    \begin{macrocode}
  def sage(self, s, l, t):
    self.sagen += 1
    return self.sout.label[self.sagen - 1]
%    \end{macrocode}
% The second method returns the appropriate |\includegraphics| command.
% It does need to account for the default argument.
%    \begin{macrocode}
  def plot(self, s, l, t):
    self.plotn += 1
    if len(t.opts) == 0:
      opts = r'[width=.75\textwidth]'
    else:
      opts = t.opts[0]
    return (r'\includegraphics%s{sage-plots-for-%s.tex/plot-%s}' %
      (opts, self.fn, self.plotn - 1))
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{SageCodeExtractor}
% This class does the opposite of the first: instead of removing Sage
% stuff and leaving only \LTX, this removes all the \LTX and leaves only
% Sage.
%    \begin{macrocode}
class SageCodeExtractor():
  def __init__(self, fn):
    smacro = sagemacroparser
    smacro.setParseAction(self.macroout)

    splot = sageplotparser
    splot.setParseAction(self.plotout)
%    \end{macrocode}
% Above, we used the general parsers for |\sage| and |\sageplot|. We
% have to redo the environment parsers because it seems too hard to
% define one parser object that will do both things we want: above, we
% just wanted to change the environment name, and here we want to suck
% out the code. Here, it's important that we find matching begin/end
% pairs; above it wasn't. At any rate, it's not a big deal to redo this
% parser.
%    \begin{macrocode}
    env_names = oneOf('sageblock sageverbatim sagesilent')
    senv = r'\begin{' + env_names('env') + '}' + SkipTo(
           r'\end{' + matchPreviousExpr(env_names) + '}')('code')
    senv.leaveWhitespace()
    senv.setParseAction(self.envout)

    spause = sagetexpause
    spause.setParseAction(self.pause)

    sunpause = sagetexunpause
    sunpause.setParseAction(self.unpause)

    doit = smacro | splot | senv | spause | sunpause

    str = ''.join(open(fn + '.tex', 'r').readlines())
    self.result = ''

    doit.transformString(str)

  def macroout(self, s, l, t):
    self.result += '# \\sage{} from line %s\n' % lineno(l, s)
    self.result += t.code[1:-1] + '\n\n'

  def plotout(self, s, l, t):
    self.result += '# \\sageplot{} from line %s:\n' % lineno(l, s)
    if t.format is not '':
      self.result += '# format: %s' % t.format[0][1:-1] + '\n'
    self.result += t.code[1:-1] + '\n\n'

  def envout(self, s, l, t):
    self.result += '# %s environment from line %s:' % (t.env,
      lineno(l, s))
    self.result += t.code[0] + '\n'

  def pause(self, s, l, t):
    self.result += ('# SageTeX (probably) paused on input line %s.\n\n' %
                    (lineno(l, s)))

  def unpause(self, s, l, t):
    self.result += ('# SageTeX (probably) unpaused on input line %s.\n\n' %
                    (lineno(l, s)))
%    \end{macrocode}
% \end{macro}

% \endinput
%</parsermod>
% Local Variables:
% mode: doctex
% TeX-master: "sagetex"
% End:
