// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
//
// 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.

/**
 * @fileoverview Protocol Buffer 2 Serializer which serializes messages
 *  into a user-friendly text format. Note that this code can run a bit
 *  slowly (especially for parsing) and should therefore not be used for
 *  time or space-critical applications.
 *
 * @see http://goo.gl/QDmDr
 */

goog.provide('goog.proto2.TextFormatSerializer');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.json');
goog.require('goog.math');
goog.require('goog.object');
goog.require('goog.proto2.FieldDescriptor');
goog.require('goog.proto2.Message');
goog.require('goog.proto2.Serializer');
goog.require('goog.string');



/**
 * TextFormatSerializer, a serializer which turns Messages into the human
 * readable text format.
 * @param {boolean=} opt_ignoreMissingFields If true, then fields that cannot be
 *     found on the proto when parsing the text format will be ignored.
 * @param {boolean=} opt_useEnumValues If true, serialization code for enums
 *     will use enum integer values instead of human-readable symbolic names.
 * @constructor
 * @extends {goog.proto2.Serializer}
 * @final
 */
goog.proto2.TextFormatSerializer = function(
    opt_ignoreMissingFields, opt_useEnumValues) {
  /**
   * Whether to ignore fields not defined on the proto when parsing the text
   * format.
   * @type {boolean}
   * @private
   */
  this.ignoreMissingFields_ = !!opt_ignoreMissingFields;

  /**
   * Whether to use integer enum values during enum serialization.
   * If false, symbolic names will be used.
   * @type {boolean}
   * @private
   */
  this.useEnumValues_ = !!opt_useEnumValues;
};
goog.inherits(goog.proto2.TextFormatSerializer, goog.proto2.Serializer);


/**
 * Deserializes a message from text format and places the data in the message.
 * @param {goog.proto2.Message} message The message in which to
 *     place the information.
 * @param {*} data The text format data.
 * @return {?string} The parse error or null on success.
 * @override
 */
goog.proto2.TextFormatSerializer.prototype.deserializeTo = function(
    message, data) {
  var descriptor = message.getDescriptor();
  var textData = data.toString();
  var parser = new goog.proto2.TextFormatSerializer.Parser();
  if (!parser.parse(message, textData, this.ignoreMissingFields_)) {
    return parser.getError();
  }

  return null;
};


/**
 * Serializes a message to a string.
 * @param {goog.proto2.Message} message The message to be serialized.
 * @return {string} The serialized form of the message.
 * @override
 */
goog.proto2.TextFormatSerializer.prototype.serialize = function(message) {
  var printer = new goog.proto2.TextFormatSerializer.Printer_();
  this.serializeMessage_(message, printer);
  return printer.toString();
};


/**
 * Serializes the message and prints the text form into the given printer.
 * @param {goog.proto2.Message} message The message to serialize.
 * @param {goog.proto2.TextFormatSerializer.Printer_} printer The printer to
 *    which the text format will be printed.
 * @private
 */
goog.proto2.TextFormatSerializer.prototype.serializeMessage_ = function(
    message, printer) {
  var descriptor = message.getDescriptor();
  var fields = descriptor.getFields();

  // Add the defined fields, recursively.
  goog.array.forEach(fields, function(field) {
    this.printField_(message, field, printer);
  }, this);

  // Add the unknown fields, if any.
  message.forEachUnknown(function(tag, value) {
    this.serializeUnknown_(tag, value, goog.asserts.assert(printer));
  }, this);
};


/**
 * Serializes an unknown field. When parsed from the JsPb object format, this
 * manifests as either a primitive type, an array, or a raw object with integer
 * keys. There is no descriptor available to interpret the types of nested
 * messages.
 * @param {number} tag The tag for the field. Since it's unknown, this is a
 *     number rather than a string.
 * @param {*} value The value of the field.
 * @param {!goog.proto2.TextFormatSerializer.Printer_} printer The printer to
 *     which the text format will be serialized.
 * @private
 */
goog.proto2.TextFormatSerializer.prototype.serializeUnknown_ = function(
    tag, value, printer) {
  if (!goog.isDefAndNotNull(value)) {
    return;
  }

  if (goog.isArray(value)) {
    goog.array.forEach(value, function(val) {
      this.serializeUnknown_(tag, val, printer);
    }, this);
    return;
  }

  if (goog.isObject(value)) {
    printer.append(tag);
    printer.append(' {');
    printer.appendLine();
    printer.indent();
    if (value instanceof goog.proto2.Message) {
      // Note(user): This conditional is here to make the
      // testSerializationOfUnknown unit test pass, but in practice we should
      // never have a Message for an "unknown" field.
      this.serializeMessage_(value, printer);
    } else {
      // For an unknown message, fields are keyed by positive integers. We
      // don't have a 'length' property to use for enumeration, so go through
      // all properties and ignore the ones that aren't legal keys.
      for (var key in value) {
        var keyAsNumber = goog.string.parseInt(key);
        goog.asserts.assert(goog.math.isInt(keyAsNumber));
        this.serializeUnknown_(keyAsNumber, value[key], printer);
      }
    }
    printer.dedent();
    printer.append('}');
    printer.appendLine();
    return;
  }

  if (goog.isString(value)) {
    value = goog.string.quote(value);
  }
  printer.append(tag);
  printer.append(': ');
  printer.append(value.toString());
  printer.appendLine();
};


/**
 * Prints the serialized value for the given field to the printer.
 * @param {*} value The field's value.
 * @param {goog.proto2.FieldDescriptor} field The field whose value is being
 *    printed.
 * @param {goog.proto2.TextFormatSerializer.Printer_} printer The printer to
 *    which the value will be printed.
 * @private
 */
goog.proto2.TextFormatSerializer.prototype.printFieldValue_ = function(
    value, field, printer) {
  switch (field.getFieldType()) {
    case goog.proto2.FieldDescriptor.FieldType.DOUBLE:
    case goog.proto2.FieldDescriptor.FieldType.FLOAT:
    case goog.proto2.FieldDescriptor.FieldType.INT64:
    case goog.proto2.FieldDescriptor.FieldType.UINT64:
    case goog.proto2.FieldDescriptor.FieldType.INT32:
    case goog.proto2.FieldDescriptor.FieldType.UINT32:
    case goog.proto2.FieldDescriptor.FieldType.FIXED64:
    case goog.proto2.FieldDescriptor.FieldType.FIXED32:
    case goog.proto2.FieldDescriptor.FieldType.BOOL:
    case goog.proto2.FieldDescriptor.FieldType.SFIXED32:
    case goog.proto2.FieldDescriptor.FieldType.SFIXED64:
    case goog.proto2.FieldDescriptor.FieldType.SINT32:
    case goog.proto2.FieldDescriptor.FieldType.SINT64:
      printer.append(value);
      break;

    case goog.proto2.FieldDescriptor.FieldType.BYTES:
    case goog.proto2.FieldDescriptor.FieldType.STRING:
      value = goog.string.quote(value.toString());
      printer.append(value);
      break;

    case goog.proto2.FieldDescriptor.FieldType.ENUM:
      if (!this.useEnumValues_) {
        // Search the enum type for a matching key.
        var found = false;
        goog.object.forEach(field.getNativeType(), function(eValue, key) {
          if (!found && eValue == value) {
            printer.append(key);
            found = true;
          }
        });
      }

      if (!found || this.useEnumValues_) {
        // Otherwise, just print the numeric value.
        printer.append(value.toString());
      }
      break;

    case goog.proto2.FieldDescriptor.FieldType.GROUP:
    case goog.proto2.FieldDescriptor.FieldType.MESSAGE:
      this.serializeMessage_(
          /** @type {goog.proto2.Message} */ (value), printer);
      break;
  }
};


/**
 * Prints the serialized field to the printer.
 * @param {goog.proto2.Message} message The parent message.
 * @param {goog.proto2.FieldDescriptor} field The field to print.
 * @param {goog.proto2.TextFormatSerializer.Printer_} printer The printer to
 *    which the field will be printed.
 * @private
 */
goog.proto2.TextFormatSerializer.prototype.printField_ = function(
    message, field, printer) {
  // Skip fields not present.
  if (!message.has(field)) {
    return;
  }

  var count = message.countOf(field);
  for (var i = 0; i < count; ++i) {
    // Field name.
    printer.append(field.getName());

    // Field delimiter.
    if (field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.MESSAGE ||
        field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.GROUP) {
      printer.append(' {');
      printer.appendLine();
      printer.indent();
    } else {
      printer.append(': ');
    }

    // Write the field value.
    this.printFieldValue_(message.get(field, i), field, printer);

    // Close the field.
    if (field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.MESSAGE ||
        field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.GROUP) {
      printer.dedent();
      printer.append('}');
      printer.appendLine();
    } else {
      printer.appendLine();
    }
  }
};


////////////////////////////////////////////////////////////////////////////////



/**
 * Helper class used by the text format serializer for pretty-printing text.
 * @constructor
 * @private
 */
goog.proto2.TextFormatSerializer.Printer_ = function() {
  /**
   * The current indentation count.
   * @type {number}
   * @private
   */
  this.indentation_ = 0;

  /**
   * The buffer of string pieces.
   * @type {Array<string>}
   * @private
   */
  this.buffer_ = [];

  /**
   * Whether indentation is required before the next append of characters.
   * @type {boolean}
   * @private
   */
  this.requiresIndentation_ = true;
};


/**
 * @return {string} The contents of the printer.
 * @override
 */
goog.proto2.TextFormatSerializer.Printer_.prototype.toString = function() {
  return this.buffer_.join('');
};


/**
 * Increases the indentation in the printer.
 */
goog.proto2.TextFormatSerializer.Printer_.prototype.indent = function() {
  this.indentation_ += 2;
};


/**
 * Decreases the indentation in the printer.
 */
goog.proto2.TextFormatSerializer.Printer_.prototype.dedent = function() {
  this.indentation_ -= 2;
  goog.asserts.assert(this.indentation_ >= 0);
};


/**
 * Appends the given value to the printer.
 * @param {*} value The value to append.
 */
goog.proto2.TextFormatSerializer.Printer_.prototype.append = function(value) {
  if (this.requiresIndentation_) {
    for (var i = 0; i < this.indentation_; ++i) {
      this.buffer_.push(' ');
    }
    this.requiresIndentation_ = false;
  }

  this.buffer_.push(value.toString());
};


/**
 * Appends a newline to the printer.
 */
goog.proto2.TextFormatSerializer.Printer_.prototype.appendLine = function() {
  this.buffer_.push('\n');
  this.requiresIndentation_ = true;
};


////////////////////////////////////////////////////////////////////////////////



/**
 * Helper class for tokenizing the text format.
 * @param {string} data The string data to tokenize.
 * @param {boolean=} opt_ignoreWhitespace If true, whitespace tokens will not
 *    be reported by the tokenizer.
 * @param {boolean=} opt_ignoreComments If true, comment tokens will not be
 *    reported by the tokenizer.
 * @constructor
 * @private
 */
goog.proto2.TextFormatSerializer.Tokenizer_ = function(
    data, opt_ignoreWhitespace, opt_ignoreComments) {

  /**
   * Whether to skip whitespace tokens on output.
   * @private {boolean}
   */
  this.ignoreWhitespace_ = !!opt_ignoreWhitespace;

  /**
   * Whether to skip comment tokens on output.
   * @private {boolean}
   */
  this.ignoreComments_ = !!opt_ignoreComments;

  /**
   * The data being tokenized.
   * @private {string}
   */
  this.data_ = data;

  /**
   * The current index in the data.
   * @private {number}
   */
  this.index_ = 0;

  /**
   * The data string starting at the current index.
   * @private {string}
   */
  this.currentData_ = data;

  /**
   * The current token type.
   * @private {goog.proto2.TextFormatSerializer.Tokenizer_.Token}
   */
  this.current_ = {
    type: goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes.END,
    value: null
  };
};


/**
 * @typedef {{type: goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes,
 *            value: ?string}}
 */
goog.proto2.TextFormatSerializer.Tokenizer_.Token;


/**
 * @return {goog.proto2.TextFormatSerializer.Tokenizer_.Token} The current
 *     token.
 */
goog.proto2.TextFormatSerializer.Tokenizer_.prototype.getCurrent = function() {
  return this.current_;
};


/**
 * An enumeration of all the token types.
 * @enum {!RegExp}
 */
goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes = {
  END: /---end---/,
  // Leading "-" to identify "-infinity"."
  IDENTIFIER: /^-?[a-zA-Z][a-zA-Z0-9_]*/,
  NUMBER: /^(0x[0-9a-f]+)|(([-])?[0-9][0-9]*(\.?[0-9]+)?(e[+-]?[0-9]+|[f])?)/,
  COMMENT: /^#.*/,
  OPEN_BRACE: /^{/,
  CLOSE_BRACE: /^}/,
  OPEN_TAG: /^</,
  CLOSE_TAG: /^>/,
  OPEN_LIST: /^\[/,
  CLOSE_LIST: /^\]/,
  STRING: new RegExp('^"([^"\\\\]|\\\\.)*"'),
  COLON: /^:/,
  COMMA: /^,/,
  SEMI: /^;/,
  WHITESPACE: /^\s/
};


/**
 * Advances to the next token.
 * @return {boolean} True if a valid token was found, false if the end was
 *    reached or no valid token was found.
 */
goog.proto2.TextFormatSerializer.Tokenizer_.prototype.next = function() {
  var types = goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes;

  // Skip any whitespace if requested.
  while (this.nextInternal_()) {
    var type = this.getCurrent().type;
    if ((type != types.WHITESPACE && type != types.COMMENT) ||
        (type == types.WHITESPACE && !this.ignoreWhitespace_) ||
        (type == types.COMMENT && !this.ignoreComments_)) {
      return true;
    }
  }

  // If we reach this point, set the current token to END.
  this.current_ = {
    type: goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes.END,
    value: null
  };

  return false;
};


/**
 * Internal method for determining the next token.
 * @return {boolean} True if a next token was found, false otherwise.
 * @private
 */
goog.proto2.TextFormatSerializer.Tokenizer_.prototype.nextInternal_ =
    function() {
  if (this.index_ >= this.data_.length) {
    return false;
  }

  var data = this.currentData_;
  var types = goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes;
  var next = null;

  // Loop through each token type and try to match the beginning of the string
  // with the token's regular expression.
  goog.object.some(types, function(type, id) {
    if (next || type == types.END) {
      return false;
    }

    // Note: This regular expression check is at, minimum, O(n).
    var info = type.exec(data);
    if (info && info.index == 0) {
      next = {type: type, value: info[0]};
    }

    return !!next;
  });

  // Advance the index by the length of the token.
  if (next) {
    this.current_ =
        /** @type {goog.proto2.TextFormatSerializer.Tokenizer_.Token} */ (next);
    this.index_ += next.value.length;
    this.currentData_ = this.currentData_.substring(next.value.length);
  }

  return !!next;
};


////////////////////////////////////////////////////////////////////////////////



/**
 * Helper class for parsing the text format.
 * @constructor
 * @final
 */
goog.proto2.TextFormatSerializer.Parser = function() {
  /**
   * The error during parsing, if any.
   * @type {?string}
   * @private
   */
  this.error_ = null;

  /**
   * The current tokenizer.
   * @type {goog.proto2.TextFormatSerializer.Tokenizer_}
   * @private
   */
  this.tokenizer_ = null;

  /**
   * Whether to ignore missing fields in the proto when parsing.
   * @type {boolean}
   * @private
   */
  this.ignoreMissingFields_ = false;
};


/**
 * Parses the given data, filling the message as it goes.
 * @param {goog.proto2.Message} message The message to fill.
 * @param {string} data The text format data.
 * @param {boolean=} opt_ignoreMissingFields If true, fields missing in the
 *     proto will be ignored.
 * @return {boolean} True on success, false on failure. On failure, the
 *     getError method can be called to get the reason for failure.
 */
goog.proto2.TextFormatSerializer.Parser.prototype.parse = function(
    message, data, opt_ignoreMissingFields) {
  this.error_ = null;
  this.ignoreMissingFields_ = !!opt_ignoreMissingFields;
  this.tokenizer_ =
      new goog.proto2.TextFormatSerializer.Tokenizer_(data, true, true);
  this.tokenizer_.next();
  return this.consumeMessage_(message, '');
};


/**
 * @return {?string} The parse error, if any.
 */
goog.proto2.TextFormatSerializer.Parser.prototype.getError = function() {
  return this.error_;
};


/**
 * Reports a parse error.
 * @param {string} msg The error message.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.reportError_ = function(msg) {
  this.error_ = msg;
};


/**
 * Attempts to consume the given message.
 * @param {goog.proto2.Message} message The message to consume and fill. If
 *    null, then the message contents will be consumed without ever being set
 *    to anything.
 * @param {string} delimiter The delimiter expected at the end of the message.
 * @return {boolean} True on success, false otherwise.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.consumeMessage_ = function(
    message, delimiter) {
  var types = goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes;
  while (!this.lookingAt_('>') && !this.lookingAt_('}') &&
         !this.lookingAtType_(types.END)) {
    if (!this.consumeField_(message)) {
      return false;
    }
  }

  if (delimiter) {
    if (!this.consume_(delimiter)) {
      return false;
    }
  } else {
    if (!this.lookingAtType_(types.END)) {
      this.reportError_('Expected END token');
    }
  }

  return true;
};


/**
 * Attempts to consume the value of the given field.
 * @param {goog.proto2.Message} message The parent message.
 * @param {goog.proto2.FieldDescriptor} field The field.
 * @return {boolean} True on success, false otherwise.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.consumeFieldValue_ = function(
    message, field) {
  var value = this.getFieldValue_(field);
  if (goog.isNull(value)) {
    return false;
  }

  if (field.isRepeated()) {
    message.add(field, value);
  } else {
    message.set(field, value);
  }

  return true;
};


/**
 * Attempts to convert a string to a number.
 * @param {string} num in hexadecimal or float format.
 * @return {number} The converted number or null on error.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.getNumberFromString_ = function(num) {

  var returnValue = goog.string.contains(num, '.') ?
      parseFloat(num) :           // num is a float.
      goog.string.parseInt(num);  // num is an int.

  goog.asserts.assert(!isNaN(returnValue));
  goog.asserts.assert(isFinite(returnValue));

  return returnValue;
};


/**
 * Parse NaN, positive infinity, or negative infinity from a string.
 * @param {string} identifier An identifier string to check.
 * @return {?number} Infinity, negative infinity, NaN, or null if none
 *     of the constants could be parsed.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.parseNumericalConstant_ = function(
    identifier) {
  if (/^-?inf(?:inity)?f?$/i.test(identifier)) {
    return Infinity * (goog.string.startsWith(identifier, '-') ? -1 : 1);
  }

  if (/^nanf?$/i.test(identifier)) {
    return NaN;
  }

  return null;
};


/**
 * Attempts to parse the given field's value from the stream.
 * @param {goog.proto2.FieldDescriptor} field The field.
 * @return {*} The field's value or null if none.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.getFieldValue_ = function(
    field) {
  var types = goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes;
  switch (field.getFieldType()) {
    case goog.proto2.FieldDescriptor.FieldType.DOUBLE:
    case goog.proto2.FieldDescriptor.FieldType.FLOAT:

      var identifier = this.consumeIdentifier_();
      if (identifier) {
        var numericalIdentifier =
            goog.proto2.TextFormatSerializer.Parser.parseNumericalConstant_(
                identifier);
        // Use isDefAndNotNull since !!NaN is false.
        if (goog.isDefAndNotNull(numericalIdentifier)) {
          return numericalIdentifier;
        }
      }

    case goog.proto2.FieldDescriptor.FieldType.INT32:
    case goog.proto2.FieldDescriptor.FieldType.UINT32:
    case goog.proto2.FieldDescriptor.FieldType.FIXED32:
    case goog.proto2.FieldDescriptor.FieldType.SFIXED32:
    case goog.proto2.FieldDescriptor.FieldType.SINT32:
      var num = this.consumeNumber_();
      if (!num) {
        return null;
      }

      return goog.proto2.TextFormatSerializer.Parser.getNumberFromString_(num);

    case goog.proto2.FieldDescriptor.FieldType.INT64:
    case goog.proto2.FieldDescriptor.FieldType.UINT64:
    case goog.proto2.FieldDescriptor.FieldType.FIXED64:
    case goog.proto2.FieldDescriptor.FieldType.SFIXED64:
    case goog.proto2.FieldDescriptor.FieldType.SINT64:
      var num = this.consumeNumber_();
      if (!num) {
        return null;
      }

      if (field.getNativeType() == Number) {
        // 64-bit number stored as a number.
        return goog.proto2.TextFormatSerializer.Parser.getNumberFromString_(
            num);
      }

      return num;  // 64-bit numbers are by default stored as strings.

    case goog.proto2.FieldDescriptor.FieldType.BOOL:
      var ident = this.consumeIdentifier_();
      if (!ident) {
        return null;
      }

      switch (ident) {
        case 'true':
          return true;
        case 'false':
          return false;
        default:
          this.reportError_('Unknown type for bool: ' + ident);
          return null;
      }

    case goog.proto2.FieldDescriptor.FieldType.ENUM:
      if (this.lookingAtType_(types.NUMBER)) {
        var num = this.consumeNumber_();
        if (!num) {
          return null;
        }

        return goog.proto2.TextFormatSerializer.Parser.getNumberFromString_(
            num);
      } else {
        // Search the enum type for a matching key.
        var name = this.consumeIdentifier_();
        if (!name) {
          return null;
        }

        var enumValue = field.getNativeType()[name];
        if (enumValue == null) {
          this.reportError_('Unknown enum value: ' + name);
          return null;
        }

        return enumValue;
      }

    case goog.proto2.FieldDescriptor.FieldType.BYTES:
    case goog.proto2.FieldDescriptor.FieldType.STRING:
      return this.consumeString_();
  }
};


/**
 * Attempts to consume a nested message.
 * @param {goog.proto2.Message} message The parent message.
 * @param {goog.proto2.FieldDescriptor} field The field.
 * @return {boolean} True on success, false otherwise.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.consumeNestedMessage_ =
    function(message, field) {
  var delimiter = '';

  // Messages support both < > and { } as delimiters for legacy reasons.
  if (this.tryConsume_('<')) {
    delimiter = '>';
  } else {
    if (!this.consume_('{')) {
      return false;
    }
    delimiter = '}';
  }

  var msg = field.getFieldMessageType().createMessageInstance();
  var result = this.consumeMessage_(msg, delimiter);
  if (!result) {
    return false;
  }

  // Add the message to the parent message.
  if (field.isRepeated()) {
    message.add(field, msg);
  } else {
    message.set(field, msg);
  }

  return true;
};


/**
 * Attempts to consume the value of an unknown field. This method uses
 * heuristics to try to consume just the right tokens.
 * @return {boolean} True on success, false otherwise.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.consumeUnknownFieldValue_ =
    function() {
  // : is optional.
  this.tryConsume_(':');

  // Handle form: [.. , ... , ..]
  if (this.tryConsume_('[')) {
    while (true) {
      this.tokenizer_.next();
      if (this.tryConsume_(']')) {
        break;
      }
      if (!this.consume_(',')) {
        return false;
      }
    }

    return true;
  }

  // Handle nested messages/groups.
  if (this.tryConsume_('<')) {
    return this.consumeMessage_(null /* unknown */, '>');
  } else if (this.tryConsume_('{')) {
    return this.consumeMessage_(null /* unknown */, '}');
  } else {
    // Otherwise, consume a single token for the field value.
    this.tokenizer_.next();
  }

  return true;
};


/**
 * Attempts to consume a field under a message.
 * @param {goog.proto2.Message} message The parent message. If null, then the
 *     field value will be consumed without being assigned to anything.
 * @return {boolean} True on success, false otherwise.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.consumeField_ = function(
    message) {
  var fieldName = this.consumeIdentifier_();
  if (!fieldName) {
    this.reportError_('Missing field name');
    return false;
  }

  var field = null;
  if (message) {
    field = message.getDescriptor().findFieldByName(fieldName.toString());
  }

  if (field == null) {
    if (this.ignoreMissingFields_) {
      return this.consumeUnknownFieldValue_();
    } else {
      this.reportError_('Unknown field: ' + fieldName);
      return false;
    }
  }

  if (field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.MESSAGE ||
      field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.GROUP) {
    // : is optional here.
    this.tryConsume_(':');
    if (!this.consumeNestedMessage_(message, field)) {
      return false;
    }
  } else {
    // Long Format: "someField: 123"
    // Short Format: "someField: [123, 456, 789]"
    if (!this.consume_(':')) {
      return false;
    }

    if (field.isRepeated() && this.tryConsume_('[')) {
      // Short repeated format, e.g.  "foo: [1, 2, 3]"
      while (true) {
        if (!this.consumeFieldValue_(message, field)) {
          return false;
        }
        if (this.tryConsume_(']')) {
          break;
        }
        if (!this.consume_(',')) {
          return false;
        }
      }
    } else {
      // Normal field format.
      if (!this.consumeFieldValue_(message, field)) {
        return false;
      }
    }
  }

  // For historical reasons, fields may optionally be separated by commas or
  // semicolons.
  this.tryConsume_(',') || this.tryConsume_(';');
  return true;
};


/**
 * Attempts to consume a token with the given string value.
 * @param {string} value The string value for the token.
 * @return {boolean} True if the token matches and was consumed, false
 *    otherwise.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.tryConsume_ = function(
    value) {
  if (this.lookingAt_(value)) {
    this.tokenizer_.next();
    return true;
  }
  return false;
};


/**
 * Consumes a token of the given type.
 * @param {goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes} type The type
 *     of the token to consume.
 * @return {?string} The string value of the token or null on error.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.consumeToken_ = function(
    type) {
  if (!this.lookingAtType_(type)) {
    this.reportError_('Expected token type: ' + type);
    return null;
  }

  var value = this.tokenizer_.getCurrent().value;
  this.tokenizer_.next();
  return value;
};


/**
 * Consumes an IDENTIFIER token.
 * @return {?string} The string value or null on error.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.consumeIdentifier_ =
    function() {
  var types = goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes;
  return this.consumeToken_(types.IDENTIFIER);
};


/**
 * Consumes a NUMBER token.
 * @return {?string} The string value or null on error.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.consumeNumber_ = function() {
  var types = goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes;
  return this.consumeToken_(types.NUMBER);
};


/**
 * Consumes a STRING token. Strings may come in multiple adjacent tokens which
 * are automatically concatenated, like in C or Python.
 * @return {?string} The *deescaped* string value or null on error.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.consumeString_ = function() {
  var types = goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes;
  var value = this.consumeToken_(types.STRING);
  if (!value) {
    return null;
  }

  var stringValue = goog.json.parse(value).toString();
  while (this.lookingAtType_(types.STRING)) {
    value = this.consumeToken_(types.STRING);
    stringValue += goog.json.parse(value).toString();
  }

  return stringValue;
};


/**
 * Consumes a token with the given value. If not found, reports an error.
 * @param {string} value The string value expected for the token.
 * @return {boolean} True on success, false otherwise.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.consume_ = function(value) {
  if (!this.tryConsume_(value)) {
    this.reportError_('Expected token "' + value + '"');
    return false;
  }

  return true;
};


/**
 * @param {string} value The value to check against.
 * @return {boolean} True if the current token has the given string value.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.lookingAt_ = function(value) {
  return this.tokenizer_.getCurrent().value == value;
};


/**
 * @param {goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes} type The
 *     token type.
 * @return {boolean} True if the current token has the given type.
 * @private
 */
goog.proto2.TextFormatSerializer.Parser.prototype.lookingAtType_ = function(
    type) {
  return this.tokenizer_.getCurrent().type == type;
};
