summaryrefslogtreecommitdiff
path: root/macros/latex/contrib/sagetex/scripts.dtx
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
committerNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
commite0c6872cf40896c7be36b11dcc744620f10adf1d (patch)
tree60335e10d2f4354b0674ec22d7b53f0f8abee672 /macros/latex/contrib/sagetex/scripts.dtx
Initial commit
Diffstat (limited to 'macros/latex/contrib/sagetex/scripts.dtx')
-rw-r--r--macros/latex/contrib/sagetex/scripts.dtx520
1 files changed, 520 insertions, 0 deletions
diff --git a/macros/latex/contrib/sagetex/scripts.dtx b/macros/latex/contrib/sagetex/scripts.dtx
new file mode 100644
index 0000000000..aec62a08cb
--- /dev/null
+++ b/macros/latex/contrib/sagetex/scripts.dtx
@@ -0,0 +1,520 @@
+% \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
+
+# 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]
+
+commented_out = r'^\s*%'
+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 not re.search(commented_out, line) and re.search(usepackage, line):
+ 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):
+ h.update(bytearray(line,'utf8'))
+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: