diff options
author | Norbert Preining <norbert@preining.info> | 2019-09-02 13:46:59 +0900 |
---|---|---|
committer | Norbert Preining <norbert@preining.info> | 2019-09-02 13:46:59 +0900 |
commit | e0c6872cf40896c7be36b11dcc744620f10adf1d (patch) | |
tree | 60335e10d2f4354b0674ec22d7b53f0f8abee672 /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.dtx | 520 |
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: |