diff options
author | Karl Berry <karl@freefriends.org> | 2016-07-21 21:18:20 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2016-07-21 21:18:20 +0000 |
commit | 24289a6c830263430a9ff06f2dd20d6ec3482079 (patch) | |
tree | de97f9224a4eae78a947c52df01eb7d1b63c37b0 /Master/texmf-dist/scripts/pythontex/pythontex_engines.py | |
parent | 75ba721d706ce924641fd8f7715a83809b757812 (diff) |
pythontex (21jul16)
git-svn-id: svn://tug.org/texlive/trunk@41746 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/scripts/pythontex/pythontex_engines.py')
-rwxr-xr-x | Master/texmf-dist/scripts/pythontex/pythontex_engines.py | 706 |
1 files changed, 502 insertions, 204 deletions
diff --git a/Master/texmf-dist/scripts/pythontex/pythontex_engines.py b/Master/texmf-dist/scripts/pythontex/pythontex_engines.py index 83ae0acd50e..6bcbd1690db 100755 --- a/Master/texmf-dist/scripts/pythontex/pythontex_engines.py +++ b/Master/texmf-dist/scripts/pythontex/pythontex_engines.py @@ -4,8 +4,8 @@ PythonTeX code engines. Provides a class for managing the different languages/types of code that may be executed. A class instance is created for each language/type of -code. The class provides a method for assembling the scripts that are -executed, combining user code with templates. It also creates the records +code. The class provides a method for assembling the scripts that are +executed, combining user code with templates. It also creates the records needed to synchronize `stderr` with the document. Each instance of the class is automatically added to the `engines_dict` upon @@ -17,7 +17,7 @@ document (script for execution). -Copyright (c) 2012-2014, Geoffrey M. Poore +Copyright (c) 2012-2016, Geoffrey M. Poore All rights reserved. Licensed under the BSD 3-Clause License: http://www.opensource.org/licenses/BSD-3-Clause @@ -28,88 +28,109 @@ Licensed under the BSD 3-Clause License: import os import sys import textwrap +import re from hashlib import sha1 from collections import OrderedDict, namedtuple -interpreter_dict = {k:k for k in ('python', 'ruby', 'julia', 'octave')} -# The {file} field needs to be replaced by itself, since the actual +interpreter_dict = {k:k for k in ('python', 'ruby', 'julia', 'octave', 'bash', 'sage', 'rustc')} +# The {file} field needs to be replaced by itself, since the actual # substitution of the real file can only be done at runtime, whereas the -# substitution for the interpreter should be done when the engine is +# substitution for the interpreter should be done when the engine is # initialized. interpreter_dict['file'] = '{file}' interpreter_dict['File'] = '{File}' +interpreter_dict['workingdir'] = '{workingdir}' engine_dict = {} -CodeIndex = namedtuple('CodeIndex', ['input_file', 'command', - 'line_int', 'lines_total', +CodeIndex = namedtuple('CodeIndex', ['input_file', 'command', + 'line_int', 'lines_total', 'lines_user', 'lines_input', 'inline_count']) class CodeEngine(object): ''' - The base class that is used for defining language engines. Each command + The base class that is used for defining language engines. Each command and environment family is based on an engine. - + The class assembles the individual scripts that PythonTeX executes, using - templates and user code. It also creates the records needed for + templates and user code. It also creates the records needed for synchronizing `stderr` with the document. ''' - def __init__(self, name, language, extension, command, template, wrapper, - formatter, errors=None, warnings=None, - linenumbers=None, lookbehind=False, + def __init__(self, name, language, extension, commands, template, wrapper, + formatter, sub=None, errors=None, warnings=None, + linenumbers=None, lookbehind=False, console=False, startup=None, created=None): # Save raw arguments so that they may be reused by subtypes - self._rawargs = (name, language, extension, command, template, wrapper, - formatter, errors, warnings, + self._rawargs = (name, language, extension, commands, template, wrapper, + formatter, sub, errors, warnings, linenumbers, lookbehind, console, startup, created) - + # Type check all strings, and make sure everything is Unicode if sys.version_info[0] == 2: - if (not isinstance(name, basestring) or - not isinstance(language, basestring) or - not isinstance(extension, basestring) or - not isinstance(command, basestring) or + if (not isinstance(name, basestring) or + not isinstance(language, basestring) or + not isinstance(extension, basestring) or not isinstance(template, basestring) or not isinstance(wrapper, basestring) or - not isinstance(formatter, basestring)): + not isinstance(formatter, basestring) or + not isinstance(sub, basestring)): raise TypeError('CodeEngine needs string in initialization') self.name = unicode(name) self.language = unicode(language) self.extension = unicode(extension) - self.command = unicode(command) self.template = unicode(template) self.wrapper = unicode(wrapper) self.formatter = unicode(formatter) + self.sub = unicode(sub) else: - if (not isinstance(name, str) or - not isinstance(language, str) or - not isinstance(extension, str) or - not isinstance(command, str) or + if (not isinstance(name, str) or + not isinstance(language, str) or + not isinstance(extension, str) or not isinstance(template, str) or not isinstance(wrapper, str) or - not isinstance(formatter, str)): + not isinstance(formatter, str) or + not isinstance(sub, str)): raise TypeError('CodeEngine needs string in initialization') self.name = name self.language = language self.extension = extension - self.command = command self.template = template self.wrapper = wrapper self.formatter = formatter + self.sub = sub # Perform some additional formatting on some strings. self.extension = self.extension.lstrip('.') self.template = self._dedent(self.template) self.wrapper = self._dedent(self.wrapper) + # Deal with commands + if sys.version_info.major == 2: + if isinstance(commands, basestring): + commands = [commands] + elif not isinstance(commands, list) and not isinstance(commands, tuple): + raise TypeError('CodeEngine needs "commands" to be a string, list, or tuple') + for c in commands: + if not isinstance(c, basestring): + raise TypeError('CodeEngine needs "commands" to contain strings') + commands = [unicode(c) for c in commands] + else: + if isinstance(commands, str): + commands = [commands] + elif not isinstance(commands, list) and not isinstance(commands, tuple): + raise TypeError('CodeEngine needs "commands" to be a string, list, or tuple') + for c in commands: + if not isinstance(c, str): + raise TypeError('CodeEngine needs "commands" to contain strings') + self.commands = commands # Make sure formatter string ends with a newline if not self.formatter.endswith('\n'): self.formatter = self.formatter + '\n' - + # Type check errors, warnings, and linenumbers if errors is None: errors = [] @@ -180,12 +201,12 @@ class CodeEngine(object): if not isinstance(lookbehind, bool): raise TypeError('CodeEngine needs "lookbehind" to be bool') self.lookbehind = lookbehind - + # Type check console if not isinstance(console, bool): raise TypeError('CodeEngine needs "console" to be bool') self.console = console - + # Type check startup if startup is None: startup = '' @@ -203,7 +224,7 @@ class CodeEngine(object): if not startup.endswith('\n'): startup += '\n' self.startup = self._dedent(startup) - + # Type check created; make sure it is an iterable and contains Unicode if created is None: created = [] @@ -226,19 +247,24 @@ class CodeEngine(object): if not isinstance(f, str): raise TypeError('CodeEngine needs "created" to contain strings') self.created = created - - # The base PythonTeX type does not support extend; it is used in + + # The base PythonTeX type does not support extend; it is used in # subtyping. But a dummy extend is needed to fill the extend field # in templates, if it is provided. self.extend = '' - + # Create dummy variables for console self.banner = '' self.filename = '' - + # Each type needs to add itself to a dict, for later access by name self._register() - + + # Regex for working with `sub` commands and environments + # Generated if used + self.sub_field_re = None + + def _dedent(self, s): ''' Dedent and strip leading newlines @@ -247,29 +273,29 @@ class CodeEngine(object): while s.startswith('\n'): s = s[1:] return s - + def _register(self): ''' Add instance to a dict for later access by name ''' engine_dict[self.name] = self - + def customize(self, **kwargs): ''' Customize the template on the fly. - - This provides customization based on command line arguments + + This provides customization based on command line arguments (`--interpreter`) and customization from the TeX side (imports from - `__future__`). Ideally, this function should be restricted to this - and similar cases. The custom code command and environment are + `__future__`). Ideally, this function should be restricted to this + and similar cases. The custom code command and environment are insufficient for such cases, because the command is at a level above - that of code and because of the requirement that imports from + that of code and because of the requirement that imports from `__future__` be at the very beginning of a script. ''' # Take care of `--interpreter` # The `interpreter_dict` has entries that allow `{file}` and # `{outputdir}` fields to be replaced with themselves - self.command = self.command.format(**interpreter_dict) + self.commands = [c.format(**interpreter_dict) for c in self.commands] # Take care of `__future__` if self.language.startswith('python'): if sys.version_info[0] == 2 and 'pyfuture' in kwargs: @@ -317,7 +343,7 @@ class CodeEngine(object): self.filename = kwargs['pyconfilename'] _hash = None - + def get_hash(self): ''' Return a hash of all vital type information (template, etc.). Create @@ -328,7 +354,8 @@ class CodeEngine(object): # the user, since a unique hash is all that's needed. if self._hash is None: hasher = sha1() - hasher.update(self.command.encode('utf8')) + for c in self.commands: + hasher.update(c.encode('utf8')) hasher.update(self.template.encode('utf8')) hasher.update(self.wrapper.encode('utf8')) hasher.update(self.formatter.encode('utf8')) @@ -338,13 +365,13 @@ class CodeEngine(object): hasher.update(self.filename.encode('utf8')) self._hash = hasher.hexdigest() return self._hash - + def _process_future(self, code_list): ''' - Go through a given list of code and extract all imports from - `__future__`, so that they can be relocated to the beginning of the + Go through a given list of code and extract all imports from + `__future__`, so that they can be relocated to the beginning of the script. - + The approach isn't foolproof and doesn't support compound statements. ''' done = False @@ -355,8 +382,8 @@ class CodeEngine(object): code = c.code.split('\n') for l, line in enumerate(code): # Detect __future__ imports - if (line.startswith('from __future__') or - line.startswith('import __future__') and + if (line.startswith('from __future__') or + line.startswith('import __future__') and not in_triplequote): changed = True if ';' in line: @@ -365,15 +392,15 @@ class CodeEngine(object): future_imports.append(line) code[l] = '' # Ignore comments, empty lines, and lines with complete docstrings - elif (line.startswith('\n') or line.startswith('#') or + elif (line.startswith('\n') or line.startswith('#') or line.isspace() or - ('"""' in line and line.count('"""')%2 == 0) or + ('"""' in line and line.count('"""')%2 == 0) or ("'''" in line and line.count("'''")%2 == 0)): pass # Detect if entering or leaving a docstring elif line.count('"""')%2 == 1 or line.count("'''")%2 == 1: in_triplequote = not in_triplequote - # Stop looking for future imports as soon as a non-comment, + # Stop looking for future imports as soon as a non-comment, # non-empty, non-docstring, non-future import line is found elif not in_triplequote: done = True @@ -386,7 +413,7 @@ class CodeEngine(object): return '\n'.join(future_imports) else: return '' - + def _get_future(self, cc_list_begin, code_list): ''' Process custom code and user code for imports from `__future__` @@ -397,31 +424,31 @@ class CodeEngine(object): return cc_future + '\n' + code_future else: return cc_future + code_future - - def get_script(self, encoding, utilspath, outputdir, workingdir, + + def get_script(self, encoding, utilspath, outputdir, workingdir, cc_list_begin, code_list, cc_list_end, debug, interactive): ''' Assemble the script that will be executed. In the process, assemble an index of line numbers that may be used to correlate script line - numbers with document line numbers and user code line numbers in the + numbers with document line numbers and user code line numbers in the event of errors or warnings. ''' lines_total = 0 script = [] code_index = OrderedDict() - + # Take care of future if self.language.startswith('python'): future = self._get_future(cc_list_begin, code_list) else: future = '' - + # Split template into beginning and ending segments try: script_begin, script_end = self.template.split('{body}') except: raise ValueError('Template for ' + self.name + ' is missing {body}') - + # Add beginning to script if os.path.isabs(os.path.expanduser(os.path.normcase(workingdir))): workingdir_full = workingdir @@ -429,12 +456,12 @@ class CodeEngine(object): workingdir_full = os.path.join(os.getcwd(), workingdir).replace('\\', '/') # Correct workingdir if in debug or interactive mode, so that it's # relative to the script path - # #### May refactor this once debugging functionality is more + # #### May refactor this once debugging functionality is more # fully implemented if debug is not None or interactive is not None: if not os.path.isabs(os.path.expanduser(os.path.normcase(workingdir))): workingdir = os.path.relpath(workingdir, outputdir) - script_begin = script_begin.format(encoding=encoding, future=future, + script_begin = script_begin.format(encoding=encoding, future=future, utilspath=utilspath, workingdir=os.path.expanduser(os.path.normcase(workingdir)), Workingdir=workingdir_full, @@ -446,7 +473,7 @@ class CodeEngine(object): created_delim='=>PYTHONTEX:CREATED#') script.append(script_begin) lines_total += script_begin.count('\n') - + # Prep wrapper try: wrapper_begin, wrapper_end = self.wrapper.split('{code}') @@ -457,7 +484,7 @@ class CodeEngine(object): # (and perhaps others) will use the line number from the NEXT # line of code that is non-empty, not from the line of code where # the error started. In these cases, it's important - # to make sure that the line number is triggered immediately + # to make sure that the line number is triggered immediately # after user code, so that the line number makes sense. Hence, # we need to strip all whitespace from the part of the wrapper # that follows user code. For symetry, we do the same for both @@ -469,9 +496,9 @@ class CodeEngine(object): wrapper_begin = wrapper_begin.replace('{stdoutdelim}', stdoutdelim).replace('{stderrdelim}', stderrdelim) wrapper_begin_offset = wrapper_begin.count('\n') wrapper_end_offset = wrapper_end.count('\n') - + # Take care of custom code - # Line counters must be reset for cc begin, code, and cc end, since + # Line counters must be reset for cc begin, code, and cc end, since # all three are separate lines_user = 0 inline_count = 0 @@ -483,7 +510,7 @@ class CodeEngine(object): args=c.args_run, instance=c.instance, line=c.line)) - + # Actual code lines_input = c.code.count('\n') code_index[c.instance] = CodeIndex(c.input_file, c.command, c.line_int, lines_total, lines_user, lines_input, inline_count) @@ -492,11 +519,11 @@ class CodeEngine(object): inline_count += 1 lines_total += lines_input lines_user += lines_input - + # Wrapper after script.append(wrapper_end) lines_total += wrapper_end_offset - + # Take care of user code lines_user = 0 inline_count = 0 @@ -508,22 +535,33 @@ class CodeEngine(object): args=c.args_run, instance=c.instance, line=c.line)) - + # Actual code - lines_input = c.code.count('\n') - code_index[c.instance] = CodeIndex(c.input_file, c.command, c.line_int, lines_total, lines_user, lines_input, inline_count) - if c.command == 'i': - script.append(self.formatter.format(code=c.code.rstrip('\n'))) - inline_count += 1 + if c.command in ('s', 'sub'): + field_list = self.process_sub(c) + code = ''.join(self.sub.format(field_delim='=>PYTHONTEX:FIELD_DELIM#', field=field) for field in field_list) + lines_input = code.count('\n') + code_index[c.instance] = CodeIndex(c.input_file, c.command, c.line_int, lines_total, lines_user, lines_input, inline_count) + script.append(code) + # #### The traceback system will need to be redone to give + # better line numbers + lines_total += lines_input + lines_user += lines_input else: - script.append(c.code) - lines_total += lines_input - lines_user += lines_input - + lines_input = c.code.count('\n') + code_index[c.instance] = CodeIndex(c.input_file, c.command, c.line_int, lines_total, lines_user, lines_input, inline_count) + if c.command == 'i': + script.append(self.formatter.format(code=c.code.rstrip('\n'))) + inline_count += 1 + else: + script.append(c.code) + lines_total += lines_input + lines_user += lines_input + # Wrapper after script.append(wrapper_end) lines_total += wrapper_end_offset - + # Take care of custom code lines_user = 0 inline_count = 0 @@ -535,7 +573,7 @@ class CodeEngine(object): args=c.args_run, instance=c.instance, line=c.line)) - + # Actual code lines_input = c.code.count('\n') code_index[c.instance] = CodeIndex(c.input_file, c.command, c.line_int, lines_total, lines_user, lines_input, inline_count) @@ -544,32 +582,105 @@ class CodeEngine(object): inline_count += 1 lines_total += lines_input lines_user += lines_input - + # Wrapper after script.append(wrapper_end) lines_total += wrapper_end_offset - + # Finish script script.append(script_end.format(dependencies_delim='=>PYTHONTEX:DEPENDENCIES#', created_delim='=>PYTHONTEX:CREATED#')) - + return script, code_index - + def process_sub(self, pytxcode): + ''' + Take the code part of a `sub` command or environment, which is + essentially an interpolation string, and extract the replacement + fields. Process the replacement fields into a form suitable for + execution and process the string into a template into which the output + may be substituted. + ''' + start = '!' + open_delim = '{' + close_delim = '}' + if self.sub_field_re is None: + field_pattern_list = [] + + # {s}: start, {o}: open_delim, {c}: close_delim + field_content_1_recursive = r'(?:[^{o}{c}\n]*|{o}R{c})+' + field_content_1_final_inner = r'[^{o}{c}\n]*' + field_1 = '{s}{o}(?!{o})' + field_content_1_recursive + '(?<!{c}){c}' + for n in range(5): # Want to allow 5 levels inside + field_1 = field_1.replace('R', field_content_1_recursive) + field_1 = field_1.replace('R', field_content_1_final_inner) + field_1 = field_1.format(s=re.escape(start), o=re.escape(open_delim), c=re.escape(close_delim)) + field_pattern_list.append(field_1) + + for n in range(2, 6+1): # Want to allow 5 levels inside + field_n = '{s}' + '{o}'*n + '(?!{o})F(?<!{c})' + '{c}'*n + field_n = field_n.replace('F', '(?:[^{o}{c}\n]*|{o}{{1,{n_minus}}}(?!{o})|{c}{{1,{n_minus}}}(?!{c}))+') + field_n = field_n.format(s=re.escape(start), o=re.escape(open_delim), c=re.escape(close_delim), n_minus=n-1) + field_pattern_list.append(field_n) + + field = '|'.join(field_pattern_list) + + escaped_start = '(?<!{s})(?:{s}{s})+(?={s}{o}|{o})'.format(s=re.escape(start), o=re.escape(open_delim)) + + pattern = ''' + (?P<escaped>{es}) | + (?P<field>{f}) | + (?P<invalid>{so}) | + (?P<text_literal_start>{s}+) | + (?P<text_general>[^{s}]+) + '''.format(es=escaped_start, f=field, so=re.escape(start + open_delim), s=re.escape(start)) + self.sub_field_re = re.compile(pattern, re.VERBOSE) + + template_list = [] + field_list = [] + field_number = 0 + for m in self.sub_field_re.finditer(pytxcode.code): + if m.lastgroup == 'escaped': + template_list.append(m.group().replace(start+start, start)) + elif m.lastgroup == 'field': + template_list.append('{{{0}}}'.format(field_number)) + field_list.append(m.group()[1:].lstrip(open_delim).rstrip(close_delim).strip()) + field_number += 1 + elif m.lastgroup.startswith('text'): + template_list.append(m.group().replace('{', '{{').replace('}', '}}')) + else: + msg = '''\ + * PythonTeX error: + Invalid "sub" command or environment. Invalid replacement fields. + {0}on or after line {1} + '''.format(pytxcode.input_file + ': ' if pytxcode.input_file else '', pytxcode.line) + msg = textwrap.dedent(msg) + sys.exit(msg) + + pytxcode.sub_template = ''.join(template_list) + + return field_list + + + + + + + class SubCodeEngine(CodeEngine): ''' Create Engine instances that inherit from existing instances. ''' - def __init__(self, base, name, language=None, extension=None, command=None, - template=None, wrapper=None, formatter=None, errors=None, - warnings=None, linenumbers=None, lookbehind=False, + def __init__(self, base, name, language=None, extension=None, commands=None, + template=None, wrapper=None, formatter=None, sub=None, + errors=None, warnings=None, linenumbers=None, lookbehind=False, console=None, created=None, startup=None, extend=None): - - self._rawargs = (name, language, extension, command, template, wrapper, - formatter, errors, warnings, + + self._rawargs = (name, language, extension, commands, template, wrapper, + formatter, sub, errors, warnings, linenumbers, lookbehind, console, startup, created) - + base_rawargs = engine_dict[base]._rawargs args = [] for n, arg in enumerate(self._rawargs): @@ -577,11 +688,11 @@ class SubCodeEngine(CodeEngine): args.append(base_rawargs[n]) else: args.append(arg) - + CodeEngine.__init__(self, *args) - + self.extend = engine_dict[base].extend - + if extend is not None: if sys.version_info[0] == 2: if not isinstance(extend, basestring): @@ -601,15 +712,15 @@ class PythonConsoleEngine(CodeEngine): ''' This uses the Engine class to store information needed for emulating Python interactive consoles. - - In the current form, it isn't used as a real engine, but rather as a - convenient storage class that keeps the treatment of all languages/code + + In the current form, it isn't used as a real engine, but rather as a + convenient storage class that keeps the treatment of all languages/code types uniform. ''' def __init__(self, name, startup=None): - CodeEngine.__init__(self, name=name, language='python', - extension='', command='', template='', - wrapper='', formatter='', errors=None, + CodeEngine.__init__(self, name=name, language='python', + extension='', commands='', template='', + wrapper='', formatter='', sub='', errors=None, warnings=None, linenumbers=None, lookbehind=False, console=True, startup=startup, created=None) @@ -618,13 +729,13 @@ class PythonConsoleEngine(CodeEngine): python_template = ''' # -*- coding: {encoding} -*- - + {future} import os import sys import codecs - + if '--interactive' not in sys.argv[1:]: if sys.version_info[0] == 2: sys.stdout = codecs.getwriter('{encoding}')(sys.stdout, 'strict') @@ -632,12 +743,12 @@ python_template = ''' else: sys.stdout = codecs.getwriter('{encoding}')(sys.stdout.buffer, 'strict') sys.stderr = codecs.getwriter('{encoding}')(sys.stderr.buffer, 'strict') - + if '{utilspath}' and '{utilspath}' not in sys.path: - sys.path.append('{utilspath}') + sys.path.append('{utilspath}') from pythontex_utils import PythonTeXUtils pytex = PythonTeXUtils() - + pytex.docdir = os.getcwd() if os.path.isdir('{workingdir}'): os.chdir('{workingdir}') @@ -648,14 +759,14 @@ python_template = ''' sys.exit('Cannot find directory {workingdir}') if pytex.docdir not in sys.path: sys.path.append(pytex.docdir) - + {extend} - + pytex.id = '{family}_{session}_{restart}' pytex.family = '{family}' pytex.session = '{session}' pytex.restart = '{restart}' - + {body} pytex.cleanup() @@ -667,25 +778,34 @@ python_wrapper = ''' pytex.args = '{args}' pytex.instance = '{instance}' pytex.line = '{line}' - + print('{stdoutdelim}') sys.stderr.write('{stderrdelim}\\n') pytex.before() - + {code} - + pytex.after() ''' +python_sub = '''print('{field_delim}')\nprint({field})\n''' + CodeEngine('python', 'python', '.py', '{python} {file}.py', - python_template, python_wrapper, 'print(pytex.formatter({code}))', - 'Error:', 'Warning:', ['line {number}', ':{number}:']) + python_template, python_wrapper, 'print(pytex.formatter({code}))', + python_sub, 'Error:', 'Warning:', ['line {number}', ':{number}:']) SubCodeEngine('python', 'py') SubCodeEngine('python', 'pylab', extend='from pylab import *') + +SubCodeEngine('python', 'sage', language='sage', extension='.sage', + template=python_template.replace('{future}', ''), + extend = 'pytex.formatter = latex', + commands='{sage} {file}.sage') + + sympy_extend = ''' from sympy import * pytex.set_formatter('sympy_latex') @@ -707,15 +827,15 @@ PythonConsoleEngine('sympycon', startup='from sympy import *') ruby_template = ''' # -*- coding: {encoding} -*- - + unless ARGV.include?('--interactive') $stdout.set_encoding('{encoding}') $stderr.set_encoding('{encoding}') end - + class RubyTeXUtils - attr_accessor :id, :family, :session, :restart, - :command, :context, :args, + attr_accessor :id, :family, :session, :restart, + :command, :context, :args, :instance, :line, :dependencies, :created, :docdir, :_context_raw def initialize @@ -770,11 +890,11 @@ ruby_template = ''' if @created @created.each {{ |x| puts x }} end - end + end end - + rbtex = RubyTeXUtils.new - + rbtex.docdir = Dir.pwd if File.directory?('{workingdir}') Dir.chdir('{workingdir}') @@ -783,14 +903,14 @@ ruby_template = ''' abort('Cannot change to directory {workingdir}') end $LOAD_PATH.push(rbtex.docdir) unless $LOAD_PATH.include?(rbtex.docdir) - + {extend} - + rbtex.id = '{family}_{session}_{restart}' rbtex.family = '{family}' rbtex.session = '{session}' rbtex.restart = '{restart}' - + {body} rbtex.cleanup @@ -802,18 +922,21 @@ ruby_wrapper = ''' rbtex.args = '{args}' rbtex.instance = '{instance}' rbtex.line = '{line}' - + puts '{stdoutdelim}' $stderr.puts '{stderrdelim}' rbtex.before - + {code} - + rbtex.after ''' -CodeEngine('ruby', 'ruby', '.rb', '{ruby} {file}.rb', ruby_template, - ruby_wrapper, 'puts rbtex.formatter({code})', +ruby_sub = '''puts '{field_delim}'\nputs {field}\n''' + + +CodeEngine('ruby', 'ruby', '.rb', '{ruby} {file}.rb', ruby_template, + ruby_wrapper, 'puts rbtex.formatter({code})', ruby_sub, ['Error)', '(Errno', 'error'], 'warning:', ':{number}:') SubCodeEngine('ruby', 'rb') @@ -823,26 +946,26 @@ SubCodeEngine('ruby', 'rb') julia_template = ''' # -*- coding: UTF-8 -*- - + # Currently, Julia only supports UTF-8 # So can't set stdout and stderr encoding - + type JuliaTeXUtils - id::String - family::String - session::String - restart::String - command::String + id::AbstractString + family::AbstractString + session::AbstractString + restart::AbstractString + command::AbstractString context::Dict - args::String - instance::String - line::String - - _dependencies::Array{{String}} - _created::Array{{String}} - docdir::String - _context_raw::String - + args::AbstractString + instance::AbstractString + line::AbstractString + + _dependencies::Array{{AbstractString}} + _created::Array{{AbstractString}} + docdir::AbstractString + _context_raw::AbstractString + formatter::Function before::Function after::Function @@ -854,26 +977,26 @@ julia_template = ''' pt_to_mm::Function pt_to_bp::Function cleanup::Function - + self::JuliaTeXUtils - + function JuliaTeXUtils() self = new() self.self = self - self._dependencies = Array(String, 0) - self._created = Array(String, 0) + self._dependencies = Array(AbstractString, 0) + self._created = Array(AbstractString, 0) self._context_raw = "" - + function formatter(expr) string(expr) end self.formatter = formatter - + function null() end self.before = null self.after = null - + function add_dependencies(files...) for file in files push!(self._dependencies, file) @@ -886,17 +1009,17 @@ julia_template = ''' end end self.add_created = add_created - + function set_context(expr) if expr != "" && expr != self._context_raw - self.context = {{strip(x[1]) => strip(x[2]) for x in map(x -> split(x, "="), split(expr, ","))}} + self.context = Dict{{Any, Any}}([ strip(x[1]) => strip(x[2]) for x in map(x -> split(x, "="), split(expr, ",")) ]) self._context_raw = expr end end self.set_context = set_context - + function pt_to_in(expr) - if isa(expr, String) + if isa(expr, AbstractString) if sizeof(expr) > 2 && expr[end-1:end] == "pt" expr = expr[1:end-2] end @@ -906,22 +1029,22 @@ julia_template = ''' end end self.pt_to_in = pt_to_in - + function pt_to_cm(expr) return self.pt_to_in(expr)*2.54 end self.pt_to_cm = pt_to_cm - + function pt_to_mm(expr) return self.pt_to_in(expr)*25.4 end self.pt_to_mm = pt_to_mm - + function pt_to_bp(expr) return self.pt_to_in(expr)*72 end self.pt_to_bp = pt_to_bp - + function cleanup() println("{dependencies_delim}") for f in self._dependencies @@ -933,13 +1056,13 @@ julia_template = ''' end end self.cleanup = cleanup - + return self end end - + jltex = JuliaTeXUtils() - + jltex.docdir = pwd() try cd("{workingdir}") @@ -950,17 +1073,17 @@ julia_template = ''' end if !(in(jltex.docdir, LOAD_PATH)) push!(LOAD_PATH, jltex.docdir) - end - + end + {extend} - + jltex.id = "{family}_{session}_{restart}" jltex.family = "{family}" jltex.session = "{session}" jltex.restart = "{restart}" - + {body} - + jltex.cleanup() ''' @@ -968,29 +1091,34 @@ julia_wrapper = ''' jltex.command = "{command}" jltex.set_context("{context}") jltex.args = "{args}" - jltex.instance = "{instance}" + jltex.instance = "{instance}" jltex.line = "{line}" - + println("{stdoutdelim}") write(STDERR, "{stderrdelim}\\n") - jltex.before() - + jltex.before() + {code} - + jltex.after() ''' -CodeEngine('julia', 'julia', '.jl', '{julia} "{file}.jl"', julia_template, - julia_wrapper, 'println(jltex.formatter({code}))', +julia_sub = '''println("{field_delim}")\nprintln({field})\n''' + + +CodeEngine('julia', 'julia', '.jl', '{julia} "{file}.jl"', julia_template, + julia_wrapper, 'println(jltex.formatter({code}))', julia_sub, 'ERROR:', 'WARNING:', ':{number}', True) SubCodeEngine('julia', 'jl') + + octave_template = ''' # Octave only supports @CLASS, not classdef # So use a struct plus functions as a substitute for a utilities class - + global octavetex = struct(); octavetex.docdir = pwd(); try @@ -1002,30 +1130,30 @@ octave_template = ''' error("Could not find directory {workingdir}"); end end - if find_dir_in_path(octavetex.docdir) + if dir_in_loadpath(octavetex.docdir) else addpath(octavetex.docdir); end - + {extend} - + octavetex.dependencies = {{}}; octavetex.created = {{}}; octavetex._context_raw = ''; - + function octavetex_formatter(argin) disp(argin); end octavetex.formatter = @(argin) octavetex_formatter(argin); - + function octavetex_before() end octavetex.before = @() octavetex_before(); - + function octavetex_after() end octavetex.after = @() octavetex_after(); - + function octavetex_add_dependencies(varargin) global octavetex; for i = 1:length(varargin) @@ -1033,7 +1161,7 @@ octave_template = ''' end end octavetex.add_dependencies = @(varargin) octavetex_add_dependencies(varargin{{:}}); - + function octavetex_add_created(varargin) global octavetex; for i = 1:length(varargin) @@ -1041,7 +1169,7 @@ octave_template = ''' end end octavetex.add_created = @(varargin) octavetex_add_created(varargin{{:}}); - + function octavetex_set_context(argin) global octavetex; if ~strcmp(argin, octavetex._context_raw) @@ -1058,7 +1186,7 @@ octave_template = ''' end end octavetex.set_context = @(argin) octavetex_set_context(argin); - + function out = octavetex_pt_to_in(argin) if ischar(argin) if length(argin) > 2 && argin(end-1:end) == 'pt' @@ -1071,22 +1199,22 @@ octave_template = ''' end end octavetex.pt_to_in = @(argin) octavetex_pt_to_in(argin); - + function out = octavetex_pt_to_cm(argin) out = octavetex_pt_to_in(argin)*2.54; end octavetex.pt_to_cm = @(argin) octavetex_pt_to_cm(argin); - + function out = octavetex_pt_to_mm(argin) out = octavetex_pt_to_in(argin)*25.4; end octavetex.pt_to_mm = @(argin) octavetex_pt_to_mm(argin); - + function out = octavetex_pt_to_bp(argin) out = octavetex_pt_to_in(argin)*72; end octavetex.pt_to_bp = @(argin) octavetex_pt_to_bp(argin); - + function octavetex_cleanup() global octavetex; fprintf(strcat('{dependencies_delim}', "\\n")); @@ -1096,18 +1224,18 @@ octave_template = ''' fprintf(strcat('{created_delim}', "\\n")); for i = 1:length(octavetex.created) fprintf(strcat(octavetex.created{{i}}, "\\n")); - end + end end octavetex.cleanup = @() octavetex_cleanup(); - + octavetex.id = '{family}_{session}_{restart}'; octavetex.family = '{family}'; octavetex.session = '{session}'; octavetex.restart = '{restart}'; - + {body} - octavetex.cleanup() + octavetex.cleanup() ''' octave_wrapper = ''' @@ -1116,17 +1244,187 @@ octave_wrapper = ''' octavetex.args = '{args}'; octavetex.instance = '{instance}'; octavetex.line = '{line}'; - - octavetex.before() - + + octavetex.before() + fprintf(strcat('{stdoutdelim}', "\\n")); fprintf(stderr, strcat('{stderrdelim}', "\\n")); {code} - + octavetex.after() ''' +octave_sub = '''disp("{field_delim}")\ndisp({field})\n''' + CodeEngine('octave', 'octave', '.m', - '{octave} -q "{File}.m"', - octave_template, octave_wrapper, 'disp({code})', + '{octave} -q "{File}.m"', + octave_template, octave_wrapper, 'disp({code})', octave_sub, 'error', 'warning', 'line {number}') + +bash_template = ''' + cd "{workingdir}" + {body} + echo "{dependencies_delim}" + echo "{created_delim}" + ''' + +bash_wrapper = ''' + echo "{stdoutdelim}" + >&2 echo "{stderrdelim}" + {code} + ''' + +bash_sub = '''echo "{field_delim}"\necho {field}\n''' + +CodeEngine('bash', 'bash', '.sh', + '{bash} "{file}.sh"', + bash_template, bash_wrapper, '{code}', bash_sub, + ['error', 'Error'], ['warning', 'Warning'], + 'line {number}') + + +rust_template = ''' + // -*- coding: utf-8 -*- + #![allow(dead_code)] + #![allow(unused_imports)] + + + mod rust_tex_utils {{ + use std::fmt; + use std::collections; + use std::io::prelude::*; + + pub struct RustTeXUtils {{ + _formatter: Box<FnMut(&fmt::Display) -> String>, + _before: Box<FnMut()>, + _after: Box<FnMut()>, + pub family: &'static str, + pub session: &'static str, + pub restart: &'static str, + pub dependencies: Vec<String>, + pub created: Vec<String>, + pub command: &'static str, + pub context: collections::HashMap<&'static str, &'static str>, + pub args: collections::HashMap<&'static str, &'static str>, + pub instance: &'static str, + pub line: &'static str, + }} + + impl RustTeXUtils {{ + pub fn new() -> Self {{ + RustTeXUtils {{ + _formatter: Box::new(|x: &fmt::Display| format!("{{}}", x)), + _before: Box::new(|| {{}}), + _after: Box::new(|| {{}}), + family: "{family}", + session: "{session}", + restart: "{restart}", + dependencies: Vec::new(), + created: Vec::new(), + command: "", + context: collections::HashMap::new(), + args: collections::HashMap::new(), + instance: "", + line: "", + }} + }} + + + pub fn formatter<A: fmt::Display>(&mut self, x: A) -> String {{ + (*self._formatter)(&x) + }} + pub fn set_formatter<F: FnMut(&fmt::Display) -> String + 'static>(&mut self, f: F) {{ + self._formatter = Box::new(f); + }} + + pub fn before(&mut self) {{ + (*self._before)(); + }} + pub fn set_before<F: FnMut() + 'static>(&mut self, f: F) {{ + self._before = Box::new(f); + }} + + pub fn after(&mut self) {{ + (*self._after)(); + }} + pub fn set_after<F: FnMut() + 'static>(&mut self, f: F) {{ + self._after = Box::new(f); + }} + + pub fn add_dependencies<SS: IntoIterator>(&mut self, deps: SS) where SS::Item: Into<String> {{ + self.dependencies.append(&mut deps.into_iter().map(|x| x.into()).collect()); + }} + + pub fn add_created<SS: IntoIterator>(&mut self, crts: SS) where SS::Item: Into<String> {{ + self.created.append(&mut crts.into_iter().map(|x| x.into()).collect()); + }} + + pub fn cleanup(self) {{ + println!("{{}}", "{dependencies_delim}"); + for x in self.dependencies {{ + println!("{{}}", x); + }} + println!("{{}}", "{created_delim}"); + for x in self.created {{ + println!("{{}}", x); + }} + }} + + pub fn setup_wrapper(&mut self, cmd: &'static str, cxt: &'static str, ags: &'static str, ist: &'static str, lne: &'static str) {{ + fn parse_map(kvs: &'static str) -> collections::HashMap<&'static str, &'static str> {{ + kvs.split(',').filter(|s| !s.is_empty()).map(|kv| {{ + let (k, v) = kv.split_at(kv.find('=').expect(&format!("Error parsing supposed key-value pair ({{}})", kv))); + (k.trim(), v[1..].trim()) + }}).collect() + }} + self.command = cmd; + self.context = parse_map(cxt); + self.args = parse_map(ags); + self.instance = ist; + self.line = lne; + }} + }} + }} + + + use std::{{io, fmt, env, path, ffi, collections}}; + use std::io::prelude::*; + + + #[allow(unused_mut)] + fn main() {{ + let mut rstex = rust_tex_utils::RustTeXUtils::new(); + if env::set_current_dir(ffi::OsString::from("{workingdir}".to_string())).is_err() && env::args().all(|x| x != "--manual") {{ + panic!("Could not change to the specified working directory ({workingdir})"); + }} + + {extend} + + {body} + + rstex.cleanup(); + }} + ''' + +rust_wrapper = ''' + rstex.setup_wrapper("{command}", "{context}", "{args}", "{instance}", "{line}"); + println!("{stdoutdelim}"); + writeln!(io::stderr(), "{stderrdelim}").unwrap(); + rstex.before(); + + {code} + + rstex.after(); + ''' + +rust_sub = '''println!("{field_delim}");\nprintln!("{{}}", {field});\n''' + +CodeEngine('rust', 'rust', '.rs', + # The full script name has to be used in order to make Windows and Unix behave nicely + # together when naming executables. Despite appearances, using `.exe` works on Unix too. + ['{rustc} --crate-type bin -o {File}.exe -L {workingdir} {file}.rs', '{File}.exe'], + rust_template, rust_wrapper, 'println!("{{}}", rstex.formatter({code}));', rust_sub, + errors='error:', warnings='warning:', linenumbers='.rs:{number}', + created='{File}.exe') + +SubCodeEngine('rust', 'rs') |