# 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) 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 = list(OrderedDict.fromkeys(features)) if len(features): lines += ['features: ' + re.sub('\n\s*', ' ', yaml.dump(features, default_flow_style=True).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 = list(OrderedDict.fromkeys(flags)) if 'async' in flags and negative and negative.get('phase') == 'parse' and negative.get('type') == 'SyntaxError': flags.remove('async') lines += ['flags: ' + re.sub('\n\s*', ' ', yaml.dump(flags, default_flow_style=True).strip())] includes = [] includes += case_values['meta'].get('includes', []) includes += self.attribs['meta'].get('includes', []) includes = list(OrderedDict.fromkeys(includes)) if len(includes): lines += ['includes: ' + re.sub('\n\s*', ' ', yaml.dump(includes, default_flow_style=True).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))