| # Copyright (C) 2016 the V8 project authors. All rights reserved. |
| # This code is governed by the BSD license found in the LICENSE file. |
| |
| import os, re |
| import codecs, yaml |
| from collections import OrderedDict |
| |
| from .util.find_comments import find_comments |
| from .util.parse_yaml import parse_yaml |
| from .test import Test |
| |
| indentPattern = re.compile(r'^(\s*)') |
| interpolatePattern = re.compile(r'\{\s*(\S+)\s*\}') |
| |
| def indent(text, prefix = ' ', js_value = False): |
| '''Prefix a block of text (as defined by the "line break" control |
| character) with some character sequence. |
| |
| :param prefix: String value to insert before each line |
| :param js_value: If True, the text will be interpreted as a JavaScript |
| value, meaning that indentation will not occur for lines that would |
| effect the runtime value; defaults to False |
| ''' |
| |
| if isinstance(text, list): |
| lines = text |
| else: |
| lines = text.split('\n') |
| |
| indented = [prefix + lines[0]] |
| str_char = None |
| |
| for line in lines[1:]: |
| # Determine if the beginning of the current line is part of some |
| # previously-opened literal value. |
| if js_value: |
| for char in indented[-1]: |
| if char == str_char: |
| str_char = None |
| elif str_char is None and char in '\'"`': |
| str_char = char |
| |
| # Do not indent the current line if it is a continuation of a literal |
| # value or if it is empty. |
| if str_char or len(line) == 0: |
| indented.append(line) |
| else: |
| indented.append(prefix + line) |
| |
| return '\n'.join(indented) |
| |
| |
| def without_duplicates(iterable): |
| return list(OrderedDict.fromkeys(iterable)) |
| |
| |
| class Template: |
| def __init__(self, filename, encoding): |
| self.filename = filename |
| |
| with codecs.open(filename, 'r', encoding) as template_file: |
| self.source = template_file.read() |
| |
| self.attribs = dict() |
| self.regions = [] |
| |
| self._parse() |
| |
| def _remove_comment(self, comment): |
| '''Create a region that is not intended to be referenced by any case, |
| ensuring that the comment is not emitted in the rendered file.''' |
| name = '__remove_comment_' + str(comment['firstchar']) + '__' |
| |
| # When a removed comment ends the line, the following newline character |
| # should also be removed from the generated file. |
| lastchar = comment['lastchar'] |
| if self.source[lastchar] == '\n': |
| comment['lastchar'] = comment['lastchar'] + 1 |
| |
| self.regions.insert(0, dict(name=name, **comment)) |
| |
| def _parse(self): |
| for comment in find_comments(self.source): |
| meta = parse_yaml(comment['source']) |
| |
| # Do not emit the template's frontmatter in generated files |
| # (file-specific frontmatter is generated as part of the rendering |
| # process) |
| if meta: |
| self.attribs['meta'] = meta |
| self._remove_comment(comment) |
| continue |
| |
| # Do not emit license information in generated files (recognized as |
| # comments preceeding the YAML frontmatter) |
| if not self.attribs.get('meta'): |
| self._remove_comment(comment) |
| continue |
| |
| match = interpolatePattern.match(comment['source']) |
| if match: |
| self.regions.insert(0, dict(name=match.group(1), **comment)) |
| |
| def expand_regions(self, source, context): |
| lines = source.split('\n') |
| |
| for region in self.regions: |
| region_name = region['name'] |
| whitespace = indentPattern.match(lines[region['lineno']]).group(1) |
| value = context['regions'].get(region_name, '') |
| |
| str_char = region.get('in_string') |
| if str_char: |
| safe_char = '"' if str_char == '\'' else '\'' |
| value = value.replace(str_char, safe_char) |
| value = value.replace('\n', '\\\n') |
| |
| if "codepoints" in context['region_options'].get(region_name, set()): |
| str_from_cp = chr |
| try: |
| # In Python 2, explicitly work on code points |
| str_from_cp = unichr |
| except NameError: pass |
| value = "".join(str_from_cp(int(cp, 16)) for cp in value.split()) |
| else: |
| value = indent(value, whitespace, True).lstrip() |
| |
| source = source[:region['firstchar']] + \ |
| value + \ |
| source[region['lastchar']:] |
| |
| setup = context['regions'].get('setup') |
| |
| if setup: |
| source = setup + '\n' + source |
| |
| teardown = context['regions'].get('teardown') |
| |
| if teardown: |
| source += '\n' + teardown + '\n' |
| |
| return source |
| |
| def _frontmatter(self, case_filename, case_values): |
| description = case_values['meta']['desc'].strip() + \ |
| ' (' + self.attribs['meta']['name'].strip() + ')' |
| lines = [] |
| |
| lines += [ |
| '// This file was procedurally generated from the following sources:', |
| '// - ' + case_filename, |
| '// - ' + self.filename, |
| '/*---', |
| 'description: ' + description, |
| ] |
| |
| esid = self.attribs['meta'].get('esid') |
| if esid: |
| lines.append('esid: ' + esid) |
| |
| es6id = self.attribs['meta'].get('es6id') |
| if es6id: |
| lines.append('es6id: ' + es6id) |
| |
| features = [] |
| features += case_values['meta'].get('features', []) |
| features += self.attribs['meta'].get('features', []) |
| features = without_duplicates(features) |
| if len(features): |
| lines += ['features: ' + yaml.dump(features, |
| default_flow_style=True, width=float('inf')).strip()] |
| |
| # Reconcile "negative" meta data before "flags" |
| if case_values['meta'].get('negative'): |
| if self.attribs['meta'].get('negative'): |
| raise Exception('Cannot specify negative in case and template file') |
| negative = case_values['meta'].get('negative') |
| else: |
| negative = self.attribs['meta'].get('negative') |
| |
| flags = ['generated'] |
| flags += case_values['meta'].get('flags', []) |
| flags += self.attribs['meta'].get('flags', []) |
| flags = without_duplicates(flags) |
| if (negative and negative.get('phase') == 'parse' |
| and negative.get('type') == 'SyntaxError' |
| and 'async' in flags): |
| flags.remove('async') |
| lines += ['flags: ' + yaml.dump(flags, default_flow_style=True, |
| width=float('inf')).strip()] |
| |
| includes = [] |
| includes += case_values['meta'].get('includes', []) |
| includes += self.attribs['meta'].get('includes', []) |
| includes = without_duplicates(includes) |
| if len(includes): |
| lines += ['includes: ' + yaml.dump(includes, |
| default_flow_style=True, width=float('inf')).strip()] |
| |
| if negative: |
| lines += ['negative:'] |
| as_yaml = yaml.dump(negative, default_flow_style=False) |
| lines += indent(as_yaml.strip(), ' ').split('\n') |
| |
| info = [] |
| |
| if 'info' in self.attribs['meta']: |
| info.append(indent(self.attribs['meta']['info'])) |
| if 'info' in case_values['meta']: |
| if len(info): |
| info.append('') |
| info.append(indent(case_values['meta']['info'])) |
| |
| if len(info): |
| lines.append('info: |') |
| lines += info |
| |
| lines.append('---*/') |
| |
| return '\n'.join(lines) |
| |
| def expand(self, case_filename, case_name, case_values, encoding): |
| assert encoding == 'utf-8' |
| def get_source(): |
| frontmatter = self._frontmatter(case_filename, case_values) |
| body = self.expand_regions(self.source, case_values) |
| return codecs.encode(frontmatter + '\n' + body, encoding) |
| return Test(self.attribs['meta']['path'] + case_name + '.js', |
| dynamic_source=get_source, |
| source_file_names=(self.filename, case_filename)) |