// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. /** * Tools for code generation. */ library codegen.tools; import 'dart:io'; import 'package:html5lib/dom.dart' as dom; import 'package:path/path.dart'; import 'text_formatter.dart'; import 'html_tools.dart'; /** * Join the given strings using camelCase. If [doCapitalize] is true, the first * part will be capitalized as well. */ String camelJoin(List parts, {bool doCapitalize: false}) { List upcasedParts = []; for (int i = 0; i < parts.length; i++) { if (i == 0 && !doCapitalize) { upcasedParts.add(parts[i]); } else { upcasedParts.add(capitalize(parts[i])); } } return upcasedParts.join(); } /** * Capitalize and return the passed String. */ String capitalize(String string) { return string[0].toUpperCase() + string.substring(1); } final RegExp trailingWhitespaceRegExp = new RegExp(r' +$', multiLine: true); /** * Mixin class for generating code. */ class CodeGenerator { _CodeGeneratorState _state; /** * Execute [callback], collecting any code that is output using [write] * or [writeln], and return the result as a string. */ String collectCode(void callback()) { _CodeGeneratorState oldState = _state; try { _state = new _CodeGeneratorState(); callback(); return _state.buffer.toString().replaceAll(trailingWhitespaceRegExp, ''); } finally { _state = oldState; } } /** * Output text without ending the current line. */ void write(Object obj) { _state.write(obj.toString()); } /** * Output text, ending the current line. */ void writeln([Object obj = '']) { _state.write('$obj\n'); } /** * Execute [callback], indenting any code it outputs by two spaces. */ void indent(void callback()) => indentSpecial(' ', ' ', callback); /** * Execute [callback], using [additionalIndent] to indent any code it outputs. */ void indentBy(String additionalIndent, void callback()) => indentSpecial(additionalIndent, additionalIndent, callback); /** * Execute [callback], using [additionalIndent] to indent any code it outputs. * The first line of output is indented by [firstAdditionalIndent] instead of * [additionalIndent]. */ void indentSpecial(String firstAdditionalIndent, String additionalIndent, void callback()) { String oldNextIndent = _state.nextIndent; String oldIndent = _state.indent; try { _state.nextIndent += firstAdditionalIndent; _state.indent += additionalIndent; callback(); } finally { _state.nextIndent = oldNextIndent; _state.indent = oldIndent; } } /** * Measure the width of the current indentation level. */ int get indentWidth => _state.nextIndent.length; /** * Generate a doc comment based on the HTML in [docs]. * * If [javadocStyle] is true, then the output is compatable with Javadoc, * which understands certain HTML constructs. */ void docComment(List docs, {int width: 79, bool javadocStyle: false}) { if (containsOnlyWhitespace(docs)) { return; } writeln('/**'); indentBy(' * ', () { write(nodesToText(docs, width - _state.indent.length, javadocStyle)); }); writeln(' */'); } void outputHeader({bool javaStyle: false}) { String header; if (javaStyle) { header = ''' /* * Copyright (c) 2014, the Dart project authors. * * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. * * This file has been automatically generated. Please do not edit it manually. * To regenerate the file, use the script "pkg/analysis_server/tool/spec/generate_files". */'''; } else { header = ''' // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. // // This file has been automatically generated. Please do not edit it manually. // To regenerate the file, use the script // "pkg/analysis_server/tool/spec/generate_files". '''; } writeln(header.trim()); } } /** * State used by [CodeGenerator]. */ class _CodeGeneratorState { StringBuffer buffer = new StringBuffer(); String nextIndent = ''; String indent = ''; bool indentNeeded = true; void write(String text) { List lines = text.split('\n'); for (int i = 0; i < lines.length; i++) { if (i == lines.length - 1 && lines[i].isEmpty) { break; } if (indentNeeded) { buffer.write(nextIndent); nextIndent = indent; } indentNeeded = false; buffer.write(lines[i]); if (i != lines.length - 1) { buffer.writeln(); indentNeeded = true; } } } } /** * Mixin class for generating HTML representations of code that are suitable * for enclosing inside a
 element.
 */
abstract class HtmlCodeGenerator {
  _HtmlCodeGeneratorState _state;

  /**
   * Execute [callback], collecting any code that is output using [write],
   * [writeln], [add], or [addAll], and return the result as a list of DOM
   * nodes.
   */
  List collectHtml(void callback()) {
    _HtmlCodeGeneratorState oldState = _state;
    try {
      _state = new _HtmlCodeGeneratorState();
      if (callback != null) {
        callback();
      }
      return _state.buffer;
    } finally {
      _state = oldState;
    }
  }

  /**
   * Add the given [node] to the HTML output.
   */
  void add(dom.Node node) {
    _state.add(node);
  }

  /**
   * Add the given [nodes] to the HTML output.
   */
  void addAll(Iterable nodes) {
    for (dom.Node node in nodes) {
      _state.add(node);
    }
  }

  /**
   * Output text without ending the current line.
   */
  void write(Object obj) {
    _state.write(obj.toString());
  }

  /**
   * Output text, ending the current line.
   */
  void writeln([Object obj = '']) {
    _state.write('$obj\n');
  }

  /**
   * Execute [callback], indenting any code it outputs by two spaces.
   */
  void indent(void callback()) {
    String oldIndent = _state.indent;
    try {
      _state.indent += '  ';
      callback();
    } finally {
      _state.indent = oldIndent;
    }
  }

  /**
   * Execute [callback], wrapping its output in an element with the given
   * [name] and [attributes].
   */
  void element(String name, Map attributes, [void callback()]) {
    add(makeElement(name, attributes, collectHtml(callback)));
  }
}

/**
 * State used by [HtmlCodeGenerator].
 */
class _HtmlCodeGeneratorState {
  List buffer = [];
  String indent = '';
  bool indentNeeded = true;

  void add(dom.Node node) {
    if (node is dom.Text) {
      write(node.text);
    } else {
      buffer.add(node);
    }
  }

  void write(String text) {
    if (text.isEmpty) {
      return;
    }
    if (indentNeeded) {
      buffer.add(new dom.Text(indent));
    }
    List lines = text.split('\n');
    if (lines.last.isEmpty) {
      lines.removeLast();
      buffer.add(new dom.Text(lines.join('\n$indent') + '\n'));
      indentNeeded = true;
    } else {
      buffer.add(new dom.Text(lines.join('\n$indent')));
      indentNeeded = false;
    }
  }
}

/**
 * Type of functions used to compute the contents of a generated file.
 */
typedef String FileContentsComputer();

/**
 * Type of functions used to compute the contents of a set of generated files.
 */
typedef Map DirectoryContentsComputer();

abstract class GeneratedContent {
  FileSystemEntity get outputFile;
  bool check();
  void generate();
}

/**
 * Class representing a single output file (either generated code or generated
 * HTML).
 */
class GeneratedFile extends GeneratedContent {
  /**
   * The output file to which generated output should be written, relative to
   * the "tool/spec" directory.  This filename uses the posix path separator
   * ('/') regardless of the OS.
   */
  final String outputPath;

  /**
   * Callback function which computes the file.
   */
  final FileContentsComputer computeContents;

  GeneratedFile(this.outputPath, this.computeContents);

  /**
   * Get a File object representing the output file.
   */
  File get outputFile => new File(joinAll(posix.split(outputPath)));

  /**
   * Check whether the file has the correct contents, and return true if it
   * does.
   */
  @override
  bool check() {
    String expectedContents = computeContents();
    try {
      return expectedContents == outputFile.readAsStringSync();
    } catch (e) {
      // There was a problem reading the file (most likely because it didn't
      // exist).  Treat that the same as if the file doesn't have the expected
      // contents.
      return false;
    }
  }

  /**
   * Replace the file with the correct contents.  [spec] is the "tool/spec"
   * directory.  If [spec] is unspecified, it is assumed to be the directory
   * containing Platform.executable.
   */
  void generate() {
    outputFile.writeAsStringSync(computeContents());
  }
}

/**
 * Class representing a single output directory (either generated code or
 * generated HTML). No other content should exisit in the directory.
 */
class GeneratedDirectory extends GeneratedContent {

  /**
   * The path to the directory that will have the generated content.
   */
  final String outputDirPath;

  /**
   * Callback function which computes the directory contents.
   */
  final DirectoryContentsComputer directoryContentsComputer;

  GeneratedDirectory(this.outputDirPath, this.directoryContentsComputer);

  /**
   * Get a Directory object representing the output directory.
   */
  Directory get outputFile =>
      new Directory(joinAll(posix.split(outputDirPath)));

  /**
   * Check whether the directory has the correct contents, and return true if it
   * does.
   */
  @override
  bool check() {
    Map map = directoryContentsComputer();
    try {
      map.forEach((String file, FileContentsComputer fileContentsComputer) {
        String expectedContents = fileContentsComputer();
        File outputFile =
            new File(joinAll(posix.split(posix.join(outputDirPath, file))));
        if (expectedContents != outputFile.readAsStringSync()) {
          return false;
        }
      });
      int nonHiddenFileCount = 0;
      outputFile.listSync(
          recursive: false,
          followLinks: false).forEach((FileSystemEntity fileSystemEntity) {
         if(fileSystemEntity is File && !basename(fileSystemEntity.path).startsWith('.')) {
           nonHiddenFileCount++;
         }
      });
      if (nonHiddenFileCount != map.length) {
        // The number of files generated doesn't match the number we expected to
        // generate.
        return false;
      }
    } catch (e) {
      // There was a problem reading the file (most likely because it didn't
      // exist).  Treat that the same as if the file doesn't have the expected
      // contents.
      return false;
    }
    return true;
  }

  /**
   * Replace the directory with the correct contents.  [spec] is the "tool/spec"
   * directory.  If [spec] is unspecified, it is assumed to be the directory
   * containing Platform.executable.
   */
  @override
  void generate() {
    try {
      // delete the contents of the directory (and the directory itself)
      outputFile.deleteSync(recursive: true);
    } catch (e) {
      // Error caught while trying to delete the directory, this can happen if
      // it didn't yet exist.
    }
    // re-create the empty directory
    outputFile.createSync(recursive: true);

    // generate all of the files in the directory
    Map map = directoryContentsComputer();
    map.forEach((String file, FileContentsComputer fileContentsComputer) {
      File outputFile = new File(joinAll(posix.split(outputDirPath + file)));
      outputFile.writeAsStringSync(fileContentsComputer());
    });
  }
}