knipknap/Gelatin

View on GitHub
Gelatin/compiler/Context.py

Summary

Maintainability
D
1 day
Test Coverage
# Copyright (c) 2010-2017 Samuel Abels
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import sys
import codecs


def do_next(context):
    return 0


def do_skip(context):
    return 1


def do_fail(context, message='No matching statement found'):
    context._error(message)


def do_say(context, message):
    context._msg(message)
    return 0


def do_warn(context, message):
    context._warn(message)
    return 0


def do_return(context, levels=1):
    # print "do.return():", -levels
    return -levels


def out_create(context, path, data=None):
    # print "out.create():", path, data
    context.builder.create(path, data)
    context.builder.enter(path)
    context._trigger(context.on_add, context.re_stack[-1])
    context.builder.leave()
    return 0


def out_replace(context, path, data=None):
    # print "out.replace():", path, data
    context.builder.add(path, data, replace=True)
    context.builder.enter(path)
    context._trigger(context.on_add, context.re_stack[-1])
    context.builder.leave()
    return 0


def out_add(context, path, data=None):
    # print "out.add():", path, data
    context.builder.add(path, data)
    context.builder.enter(path)
    context._trigger(context.on_add, context.re_stack[-1])
    context.builder.leave()
    return 0


def out_add_attribute(context, path, name, value):
    # print "out.add_attribute():", path, name, value
    context.builder.add_attribute(path, name, value)
    context.builder.enter(path)
    context._trigger(context.on_add, context.re_stack[-1])
    context.builder.leave()
    return 0


def out_open(context, path):
    # print "out.open():", path
    context.builder.open(path)
    context._trigger(context.on_add, context.re_stack[-1])
    context.stack[-1].on_leave.append((context.builder.leave, ()))
    return 0


def out_enter(context, path):
    # print "out.enter():", path
    context.builder.enter(path)
    context._trigger(context.on_add, context.re_stack[-1])
    context.stack[-1].on_leave.append((context.builder.leave, ()))
    return 0


def out_enqueue_before(context, regex, path, data=None):
    # print "ENQ BEFORE", regex.pattern, path, data
    context.on_match_before.append((regex, out_add, (context, path, data)))
    return 0


def out_enqueue_after(context, regex, path, data=None):
    # print "ENQ AFTER", regex.pattern, path, data
    context.on_match_after.append((regex, out_add, (context, path, data)))
    return 0


def out_enqueue_on_add(context, regex, path, data=None):
    # print "ENQ ON ADD", regex.pattern, path, data
    context.on_add.append((regex, out_add, (context, path, data)))
    return 0


def out_clear_queue(context):
    context._clear_triggers()
    return 1


def out_set_root_name(context, name):
    context.builder.set_root_name(name)
    return 0


class Context(object):

    def __init__(self):
        self.functions = {'do.fail':            do_fail,
                          'do.return':          do_return,
                          'do.next':            do_next,
                          'do.skip':            do_skip,
                          'do.say':             do_say,
                          'do.warn':            do_warn,
                          'out.create':         out_create,
                          'out.replace':        out_replace,
                          'out.add':            out_add,
                          'out.add_attribute':  out_add_attribute,
                          'out.open':           out_open,
                          'out.enter':          out_enter,
                          'out.enqueue_before': out_enqueue_before,
                          'out.enqueue_after':  out_enqueue_after,
                          'out.enqueue_on_add': out_enqueue_on_add,
                          'out.clear_queue':    out_clear_queue,
                          'out.set_root_name':  out_set_root_name}
        self.lexicon = {}
        self.grammars = {}
        self.input = None
        self.builder = None
        self.end = 0
        self._init()

    def _init(self):
        self.start = 0
        self.re_stack = []
        self.stack = []
        self._clear_triggers()

    def _clear_triggers(self):
        self.on_match_before = []
        self.on_match_after = []
        self.on_add = []

    def _trigger(self, triggers, match):
        matching = []
        for trigger in triggers:
            regex, func, args = trigger
            if regex.search(match.group(0)) is not None:
                matching.append(trigger)
        for trigger in matching:
            triggers.remove(trigger)
        for trigger in matching:
            regex, func, args = trigger
            func(*args)

    def _match_before_notify(self, match):
        self.re_stack.append(match)
        self._trigger(self.on_match_before, match)

    def _match_after_notify(self, match):
        self._trigger(self.on_match_after, match)
        self.re_stack.pop()

    def _get_lineno(self):
        return self.input.count('\n', 0, self.start) + 1

    def _get_line(self, number=None):
        if number is None:
            number = self._get_lineno()
        return self.input.split('\n')[number - 1]

    def _get_line_position_from_char(self, char):
        line_start = char
        while line_start != 0:
            if self.input[line_start - 1] == '\n':
                break
            line_start -= 1
        line_end = self.input.find('\n', char)
        return line_start, line_end

    def _format(self, error):
        start, end = self._get_line_position_from_char(self.start)
        line_number = self._get_lineno()
        line = self._get_line()
        offset = self.start - start
        token_len = 1
        output = line + '\n'
        if token_len <= 1:
            output += (' ' * offset) + '^\n'
        else:
            output += (' ' * offset) + "'" + ('-' * (token_len - 2)) + "'\n"
        output += '%s in line %s' % (error, line_number)
        return output

    def _msg(self, error):
        print(self._format(error))

    def _warn(self, error):
        sys.stderr.write(self._format(error) + '\n')

    def _error(self, error):
        raise Exception(self._format(error))

    def _eof(self):
        return self.start >= self.end

    def parse_string(self, input, builder, debug=0):
        self._init()
        self.input = input
        self.builder = builder
        self.end = len(input)
        self.grammars['input'].parse(self, debug)
        if self.start < self.end:
            self._error('parser returned, but did not complete')

    def parse(self, filename, builder, encoding='utf8', debug=0):
        with codecs.open(filename, 'r', encoding=encoding) as input_file:
            return self.parse_string(input_file.read(), builder, debug)

    def dump(self):
        for grammar in self.grammars.values():
            print(grammar)