diff --git a/AUTHORS b/AUTHORS
index 9f1e5d6..faf4eca 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -856,6 +856,7 @@
 Xuefei Ren <xrenishere@gmail.com>
 Xueqing Huang <huangxueqing@xiaomi.com>
 Xun Sun <xun.sun@intel.com>
+Xunran Ding <dingxunran@gmail.com>
 Yael Aharon <yael.aharon@intel.com>
 Yair Yogev <progame@chromium.org>
 Yan Wang <yan0422.wang@samsung.com>
diff --git a/DEPS b/DEPS
index 070d393..02f64fd 100644
--- a/DEPS
+++ b/DEPS
@@ -86,7 +86,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ANGLE
   # and whatever else without interference from each other.
-  'angle_revision': '54a29ffd82e7782c764b5257365e7f148f48ca4a',
+  'angle_revision': '361df070323f430aa0614c6eb04fb595dec3daa9',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling build tools
   # and whatever else without interference from each other.
@@ -98,7 +98,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling PDFium
   # and whatever else without interference from each other.
-  'pdfium_revision': '1980f10ff2b869f14c409b712eea6744941ebd88',
+  'pdfium_revision': '1f0d1fda6db83ee402561902c76ae8a6da124663',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling openmax_dl
   # and whatever else without interference from each other.
@@ -130,7 +130,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling catapult
   # and whatever else without interference from each other.
-  'catapult_revision': '259a1ec4b05d169f5027345828ca77ef7bec6ec6',
+  'catapult_revision': '9252115c46302971355d1d5fcbe1512f32d30a70',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libFuzzer
   # and whatever else without interference from each other.
@@ -626,7 +626,7 @@
     Var('chromium_git') + '/external/selenium/py.git' + '@' + '5fd78261a75fe08d27ca4835fb6c5ce4b42275bd',
 
   'src/third_party/webgl/src':
-    Var('chromium_git') + '/external/khronosgroup/webgl.git' + '@' + 'e4919fa03c74bd561dcabf3e61668fa3c7e54353',
+    Var('chromium_git') + '/external/khronosgroup/webgl.git' + '@' + '05591bbeae6592fd924caec8e728a4ea86cbb8c9',
 
   'src/third_party/webrtc':
     Var('webrtc_git') + '/src.git' + '@' + '4e70a72571dd26b85c2385e9c618e343428df5d3', # commit position 20628
diff --git a/base/android/java/src/org/chromium/base/LocaleUtils.java b/base/android/java/src/org/chromium/base/LocaleUtils.java
index 2f51455..85a62b98 100644
--- a/base/android/java/src/org/chromium/base/LocaleUtils.java
+++ b/base/android/java/src/org/chromium/base/LocaleUtils.java
@@ -32,7 +32,7 @@
 
     static {
         // A variation of this mapping also exists in:
-        // build/android/gyp/package_resources.py
+        // build/android/gyp/process_resources.py
         HashMap<String, String> mapForChromium = new HashMap<>();
         mapForChromium.put("iw", "he"); // Hebrew
         mapForChromium.put("ji", "yi"); // Yiddish
diff --git a/base/files/file_win.cc b/base/files/file_win.cc
index 6e7c383..3f5547d 100644
--- a/base/files/file_win.cc
+++ b/base/files/file_win.cc
@@ -348,6 +348,8 @@
   }
 
   if (!disposition) {
+    ::SetLastError(ERROR_INVALID_PARAMETER);
+    error_details_ = FILE_ERROR_FAILED;
     NOTREACHED();
     return;
   }
diff --git a/build/android/gyp/package_resources.py b/build/android/gyp/package_resources.py
deleted file mode 100755
index 2be10bb..0000000
--- a/build/android/gyp/package_resources.py
+++ /dev/null
@@ -1,449 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright 2014 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-# pylint: disable=C0301
-"""Package resources into an apk.
-
-See https://android.googlesource.com/platform/tools/base/+/master/legacy/ant-tasks/src/main/java/com/android/ant/AaptExecTask.java
-and
-https://android.googlesource.com/platform/sdk/+/master/files/ant/build.xml
-"""
-# pylint: enable=C0301
-
-import multiprocessing.pool
-import optparse
-import os
-import re
-import shutil
-import subprocess
-import sys
-import zipfile
-
-from util import build_utils
-
-
-# A variation of this lists also exists in:
-# //base/android/java/src/org/chromium/base/LocaleUtils.java
-_CHROME_TO_ANDROID_LOCALE_MAP = {
-    'en-GB': 'en-rGB',
-    'en-US': 'en-rUS',
-    'es-419': 'es-rUS',
-    'fil': 'tl',
-    'he': 'iw',
-    'id': 'in',
-    'pt-PT': 'pt-rPT',
-    'pt-BR': 'pt-rBR',
-    'yi': 'ji',
-    'zh-CN': 'zh-rCN',
-    'zh-TW': 'zh-rTW',
-}
-
-# List is generated from the chrome_apk.apk_intermediates.ap_ via:
-#     unzip -l $FILE_AP_ | cut -c31- | grep res/draw | cut -d'/' -f 2 | sort \
-#     | uniq | grep -- -tvdpi- | cut -c10-
-# and then manually sorted.
-# Note that we can't just do a cross-product of dimensions because the filenames
-# become too big and aapt fails to create the files.
-# This leaves all default drawables (mdpi) in the main apk. Android gets upset
-# though if any drawables are missing from the default drawables/ directory.
-DENSITY_SPLITS = {
-    'hdpi': (
-        'hdpi-v4', # Order matters for output file names.
-        'ldrtl-hdpi-v4',
-        'sw600dp-hdpi-v13',
-        'ldrtl-hdpi-v17',
-        'ldrtl-sw600dp-hdpi-v17',
-        'hdpi-v21',
-    ),
-    'xhdpi': (
-        'xhdpi-v4',
-        'ldrtl-xhdpi-v4',
-        'sw600dp-xhdpi-v13',
-        'ldrtl-xhdpi-v17',
-        'ldrtl-sw600dp-xhdpi-v17',
-        'xhdpi-v21',
-    ),
-    'xxhdpi': (
-        'xxhdpi-v4',
-        'ldrtl-xxhdpi-v4',
-        'sw600dp-xxhdpi-v13',
-        'ldrtl-xxhdpi-v17',
-        'ldrtl-sw600dp-xxhdpi-v17',
-        'xxhdpi-v21',
-    ),
-    'xxxhdpi': (
-        'xxxhdpi-v4',
-        'ldrtl-xxxhdpi-v4',
-        'sw600dp-xxxhdpi-v13',
-        'ldrtl-xxxhdpi-v17',
-        'ldrtl-sw600dp-xxxhdpi-v17',
-        'xxxhdpi-v21',
-    ),
-    'tvdpi': (
-        'tvdpi-v4',
-        'sw600dp-tvdpi-v13',
-        'ldrtl-sw600dp-tvdpi-v17',
-    ),
-}
-
-
-_PNG_TO_WEBP_ARGS = [
-    '-mt', '-quiet', '-m', '6', '-q', '100', '-lossless', '-o']
-
-
-def _ParseArgs(args):
-  """Parses command line options.
-
-  Returns:
-    An options object as from optparse.OptionsParser.parse_args()
-  """
-  parser = optparse.OptionParser()
-  build_utils.AddDepfileOption(parser)
-  parser.add_option('--android-sdk-jar',
-                    help='path to the Android SDK jar.')
-  parser.add_option('--aapt-path',
-                    help='path to the Android aapt tool')
-  parser.add_option('--debuggable',
-                    action='store_true',
-                    help='Whether to add android:debuggable="true"')
-  parser.add_option('--android-manifest', help='AndroidManifest.xml path')
-  parser.add_option('--version-code', help='Version code for apk.')
-  parser.add_option('--version-name', help='Version name for apk.')
-  parser.add_option(
-      '--shared-resources',
-      action='store_true',
-      help='Make a resource package that can be loaded by a different'
-      'application at runtime to access the package\'s resources.')
-  parser.add_option(
-      '--app-as-shared-lib',
-      action='store_true',
-      help='Make a resource package that can be loaded as shared library')
-  parser.add_option('--resource-zips',
-                    default='[]',
-                    help='zip files containing resources to be packaged')
-  parser.add_option('--asset-dir',
-                    help='directories containing assets to be packaged')
-  parser.add_option('--no-compress', help='disables compression for the '
-                    'given comma separated list of extensions')
-  parser.add_option(
-      '--create-density-splits',
-      action='store_true',
-      help='Enables density splits')
-  parser.add_option('--language-splits',
-                    default='[]',
-                    help='GN list of languages to create splits for')
-  parser.add_option('--locale-whitelist',
-                    default='[]',
-                    help='GN list of languages to include. All other language '
-                         'configs will be stripped out. List may include '
-                         'a combination of Android locales or Chrome locales.')
-  parser.add_option('--apk-path',
-                    help='Path to output (partial) apk.')
-  parser.add_option('--exclude-xxxhdpi', action='store_true',
-                    help='Do not include xxxhdpi drawables.')
-  parser.add_option('--xxxhdpi-whitelist',
-                    default='[]',
-                    help='GN list of globs that say which xxxhdpi images to '
-                         'include even when --exclude-xxxhdpi is set.')
-  parser.add_option('--png-to-webp', action='store_true',
-                    help='Convert png files to webp format.')
-  parser.add_option('--webp-binary', default='',
-                    help='Path to the cwebp binary.')
-  parser.add_option('--support-zh-hk', action='store_true',
-                    help='Tell aapt to support zh-rHK.')
-
-  options, positional_args = parser.parse_args(args)
-
-  if positional_args:
-    parser.error('No positional arguments should be given.')
-
-  # Check that required options have been provided.
-  required_options = ('android_sdk_jar', 'aapt_path', 'android_manifest',
-                      'version_code', 'version_name', 'apk_path')
-
-  build_utils.CheckOptions(options, parser, required=required_options)
-
-  options.resource_zips = build_utils.ParseGnList(options.resource_zips)
-  options.language_splits = build_utils.ParseGnList(options.language_splits)
-  options.locale_whitelist = build_utils.ParseGnList(options.locale_whitelist)
-  options.xxxhdpi_whitelist = build_utils.ParseGnList(options.xxxhdpi_whitelist)
-  return options
-
-
-def _ToAaptLocales(locale_whitelist, support_zh_hk):
-  """Converts the list of Chrome locales to aapt config locales."""
-  ret = set()
-  for locale in locale_whitelist:
-    locale = _CHROME_TO_ANDROID_LOCALE_MAP.get(locale, locale)
-    if locale is None or ('-' in locale and '-r' not in locale):
-      raise Exception('_CHROME_TO_ANDROID_LOCALE_MAP needs updating.'
-                      ' Found: %s' % locale)
-    ret.add(locale)
-    # Always keep non-regional fall-backs.
-    language = locale.split('-')[0]
-    ret.add(language)
-
-  # We don't actually support zh-HK in Chrome on Android, but we mimic the
-  # native side behavior where we use zh-TW resources when the locale is set to
-  # zh-HK. See https://crbug.com/780847.
-  if support_zh_hk:
-    assert not any('HK' in l for l in locale_whitelist), (
-        'Remove special logic if zh-HK is now supported (crbug.com/780847).')
-    ret.add('zh-rHK')
-  return sorted(ret)
-
-
-def MoveImagesToNonMdpiFolders(res_root):
-  """Move images from drawable-*-mdpi-* folders to drawable-* folders.
-
-  Why? http://crbug.com/289843
-  """
-  for src_dir_name in os.listdir(res_root):
-    src_components = src_dir_name.split('-')
-    if src_components[0] != 'drawable' or 'mdpi' not in src_components:
-      continue
-    src_dir = os.path.join(res_root, src_dir_name)
-    if not os.path.isdir(src_dir):
-      continue
-    dst_components = [c for c in src_components if c != 'mdpi']
-    assert dst_components != src_components
-    dst_dir_name = '-'.join(dst_components)
-    dst_dir = os.path.join(res_root, dst_dir_name)
-    build_utils.MakeDirectory(dst_dir)
-    for src_file_name in os.listdir(src_dir):
-      if not src_file_name.endswith('.png'):
-        continue
-      src_file = os.path.join(src_dir, src_file_name)
-      dst_file = os.path.join(dst_dir, src_file_name)
-      assert not os.path.lexists(dst_file)
-      shutil.move(src_file, dst_file)
-
-
-def PackageArgsForExtractedZip(d):
-  """Returns the aapt args for an extracted resources zip.
-
-  A resources zip either contains the resources for a single target or for
-  multiple targets. If it is multiple targets merged into one, the actual
-  resource directories will be contained in the subdirectories 0, 1, 2, ...
-  """
-  subdirs = [os.path.join(d, s) for s in os.listdir(d)]
-  subdirs = [s for s in subdirs if os.path.isdir(s)]
-  is_multi = any(os.path.basename(s).isdigit() for s in subdirs)
-  if is_multi:
-    res_dirs = sorted(subdirs, key=lambda p : int(os.path.basename(p)))
-  else:
-    res_dirs = [d]
-  package_command = []
-  for d in res_dirs:
-    MoveImagesToNonMdpiFolders(d)
-    package_command += ['-S', d]
-  return package_command
-
-
-def _GenerateDensitySplitPaths(apk_path):
-  for density, config in DENSITY_SPLITS.iteritems():
-    src_path = '%s_%s' % (apk_path, '_'.join(config))
-    dst_path = '%s_%s' % (apk_path, density)
-    yield src_path, dst_path
-
-
-def _GenerateLanguageSplitOutputPaths(apk_path, languages):
-  for lang in languages:
-    yield '%s_%s' % (apk_path, lang)
-
-
-def RenameDensitySplits(apk_path):
-  """Renames all density splits to have shorter / predictable names."""
-  for src_path, dst_path in _GenerateDensitySplitPaths(apk_path):
-    shutil.move(src_path, dst_path)
-
-
-def CheckForMissedConfigs(apk_path, check_density, languages):
-  """Raises an exception if apk_path contains any unexpected configs."""
-  triggers = []
-  if check_density:
-    triggers.extend(re.compile('-%s' % density) for density in DENSITY_SPLITS)
-  if languages:
-    triggers.extend(re.compile(r'-%s\b' % lang) for lang in languages)
-  with zipfile.ZipFile(apk_path) as main_apk_zip:
-    for name in main_apk_zip.namelist():
-      for trigger in triggers:
-        if trigger.search(name) and not 'mipmap-' in name:
-          raise Exception(('Found config in main apk that should have been ' +
-                           'put into a split: %s\nYou need to update ' +
-                           'package_resources.py to include this new ' +
-                           'config (trigger=%s)') % (name, trigger.pattern))
-
-
-def _ConstructMostAaptArgs(options):
-  package_command = [
-      options.aapt_path,
-      'package',
-      '--version-code', options.version_code,
-      '--version-name', options.version_name,
-      '-M', options.android_manifest,
-      '--no-crunch',
-      '-f',
-      '--auto-add-overlay',
-      '--no-version-vectors',
-      '-I', options.android_sdk_jar,
-      '-F', options.apk_path,
-      '--ignore-assets', build_utils.AAPT_IGNORE_PATTERN,
-  ]
-
-  if options.no_compress:
-    for ext in options.no_compress.split(','):
-      package_command += ['-0', ext]
-
-  if options.shared_resources:
-    package_command.append('--shared-lib')
-
-  if options.app_as_shared_lib:
-    package_command.append('--app-as-shared-lib')
-
-  if options.asset_dir and os.path.exists(options.asset_dir):
-    package_command += ['-A', options.asset_dir]
-
-  if options.create_density_splits:
-    for config in DENSITY_SPLITS.itervalues():
-      package_command.extend(('--split', ','.join(config)))
-
-  if options.language_splits:
-    for lang in options.language_splits:
-      package_command.extend(('--split', lang))
-
-  if options.debuggable:
-    package_command += ['--debug-mode']
-
-  if options.locale_whitelist:
-    aapt_locales = _ToAaptLocales(
-        options.locale_whitelist, options.support_zh_hk)
-    package_command += ['-c', ','.join(aapt_locales)]
-
-  return package_command
-
-
-def _ResourceNameFromPath(path):
-  return os.path.splitext(os.path.basename(path))[0]
-
-
-def _CreateExtractPredicate(dep_zips, exclude_xxxhdpi, xxxhdpi_whitelist):
-  if not exclude_xxxhdpi:
-    # Do not extract dotfiles (e.g. ".gitkeep"). aapt ignores them anyways.
-    return lambda path: os.path.basename(path)[0] != '.'
-
-  # Returns False only for xxxhdpi non-mipmap, non-whitelisted drawables.
-  naive_predicate = lambda path: (
-      not re.search(r'[/-]xxxhdpi[/-]', path) or
-      re.search(r'[/-]mipmap[/-]', path) or
-      build_utils.MatchesGlob(path, xxxhdpi_whitelist))
-
-  # Build a set of all non-xxxhdpi drawables to ensure that we never exclude any
-  # xxxhdpi drawable that does not exist in other densities.
-  non_xxxhdpi_drawables = set()
-  for resource_zip_path in dep_zips:
-    with zipfile.ZipFile(resource_zip_path) as zip_file:
-      for path in zip_file.namelist():
-        if re.search(r'[/-]drawable[/-]', path) and naive_predicate(path):
-          non_xxxhdpi_drawables.add(_ResourceNameFromPath(path))
-
-  return lambda path: (naive_predicate(path) or
-                       _ResourceNameFromPath(path) not in non_xxxhdpi_drawables)
-
-
-def _ConvertToWebP(webp_binary, png_files):
-  pool = multiprocessing.pool.ThreadPool(10)
-  def convert_image(png_path):
-    root = os.path.splitext(png_path)[0]
-    webp_path = root + '.webp'
-    args = [webp_binary, png_path] + _PNG_TO_WEBP_ARGS + [webp_path]
-    subprocess.check_call(args)
-    os.remove(png_path)
-  # Android requires pngs for 9-patch images.
-  pool.map(convert_image, [f for f in png_files if not f.endswith('.9.png')])
-  pool.close()
-  pool.join()
-
-
-def _OnStaleMd5(package_command, options):
-  with build_utils.TempDir() as temp_dir:
-    if options.resource_zips:
-      dep_zips = options.resource_zips
-      extract_predicate = _CreateExtractPredicate(
-          dep_zips, options.exclude_xxxhdpi, options.xxxhdpi_whitelist)
-      png_paths = []
-      package_subdirs = []
-      for z in dep_zips:
-        subdir = os.path.join(temp_dir, os.path.basename(z))
-        if os.path.exists(subdir):
-          raise Exception('Resource zip name conflict: ' + os.path.basename(z))
-        extracted_files = build_utils.ExtractAll(
-            z, path=subdir, predicate=extract_predicate)
-        if extracted_files:
-          package_subdirs.append(subdir)
-          png_paths.extend(f for f in extracted_files if f.endswith('.png'))
-      if png_paths and options.png_to_webp:
-        _ConvertToWebP(options.webp_binary, png_paths)
-      for subdir in package_subdirs:
-        package_command += PackageArgsForExtractedZip(subdir)
-
-    build_utils.CheckOutput(
-        package_command, print_stdout=False, print_stderr=False)
-
-    if options.create_density_splits or options.language_splits:
-      CheckForMissedConfigs(options.apk_path, options.create_density_splits,
-                            options.language_splits)
-
-    if options.create_density_splits:
-      RenameDensitySplits(options.apk_path)
-
-
-def main(args):
-  args = build_utils.ExpandFileArgs(args)
-  options = _ParseArgs(args)
-
-  package_command = _ConstructMostAaptArgs(options)
-
-  output_paths = [options.apk_path]
-
-  if options.create_density_splits:
-    for _, dst_path in _GenerateDensitySplitPaths(options.apk_path):
-      output_paths.append(dst_path)
-  output_paths.extend(
-      _GenerateLanguageSplitOutputPaths(options.apk_path,
-                                        options.language_splits))
-
-  input_paths = [options.android_manifest] + options.resource_zips
-
-  input_strings = [options.exclude_xxxhdpi] + options.xxxhdpi_whitelist
-  input_strings.extend(package_command)
-  if options.png_to_webp:
-    # This is necessary to ensure conversion if the option is toggled.
-    input_strings.append('png_to_webp')
-  if options.support_zh_hk:
-    input_strings.append('support_zh_hk')
-
-  # The md5_check.py doesn't count file path in md5 intentionally,
-  # in order to repackage resources when assets' name changed, we need
-  # to put assets into input_strings, as we know the assets path isn't
-  # changed among each build if there is no asset change.
-  if options.asset_dir and os.path.exists(options.asset_dir):
-    asset_paths = []
-    for root, _, filenames in os.walk(options.asset_dir):
-      asset_paths.extend(os.path.join(root, f) for f in filenames)
-    input_paths.extend(asset_paths)
-    input_strings.extend(sorted(asset_paths))
-
-  build_utils.CallAndWriteDepfileIfStale(
-      lambda: _OnStaleMd5(package_command, options),
-      options,
-      input_paths=input_paths,
-      input_strings=input_strings,
-      output_paths=output_paths)
-
-
-if __name__ == '__main__':
-  main(sys.argv[1:])
diff --git a/build/android/gyp/process_resources.py b/build/android/gyp/process_resources.py
index 0df462e9..31a87de 100755
--- a/build/android/gyp/process_resources.py
+++ b/build/android/gyp/process_resources.py
@@ -12,10 +12,12 @@
 
 import codecs
 import collections
+import multiprocessing.pool
 import optparse
 import os
 import re
 import shutil
+import subprocess
 import sys
 import xml.etree.ElementTree
 import zipfile
@@ -35,6 +37,73 @@
     ('java_type', 'resource_type', 'name', 'value'))
 
 
+# A variation of this lists also exists in:
+# //base/android/java/src/org/chromium/base/LocaleUtils.java
+_CHROME_TO_ANDROID_LOCALE_MAP = {
+    'en-GB': 'en-rGB',
+    'en-US': 'en-rUS',
+    'es-419': 'es-rUS',
+    'fil': 'tl',
+    'he': 'iw',
+    'id': 'in',
+    'pt-PT': 'pt-rPT',
+    'pt-BR': 'pt-rBR',
+    'yi': 'ji',
+    'zh-CN': 'zh-rCN',
+    'zh-TW': 'zh-rTW',
+}
+
+# List is generated from the chrome_apk.apk_intermediates.ap_ via:
+#     unzip -l $FILE_AP_ | cut -c31- | grep res/draw | cut -d'/' -f 2 | sort \
+#     | uniq | grep -- -tvdpi- | cut -c10-
+# and then manually sorted.
+# Note that we can't just do a cross-product of dimensions because the filenames
+# become too big and aapt fails to create the files.
+# This leaves all default drawables (mdpi) in the main apk. Android gets upset
+# though if any drawables are missing from the default drawables/ directory.
+_DENSITY_SPLITS = {
+    'hdpi': (
+        'hdpi-v4', # Order matters for output file names.
+        'ldrtl-hdpi-v4',
+        'sw600dp-hdpi-v13',
+        'ldrtl-hdpi-v17',
+        'ldrtl-sw600dp-hdpi-v17',
+        'hdpi-v21',
+    ),
+    'xhdpi': (
+        'xhdpi-v4',
+        'ldrtl-xhdpi-v4',
+        'sw600dp-xhdpi-v13',
+        'ldrtl-xhdpi-v17',
+        'ldrtl-sw600dp-xhdpi-v17',
+        'xhdpi-v21',
+    ),
+    'xxhdpi': (
+        'xxhdpi-v4',
+        'ldrtl-xxhdpi-v4',
+        'sw600dp-xxhdpi-v13',
+        'ldrtl-xxhdpi-v17',
+        'ldrtl-sw600dp-xxhdpi-v17',
+        'xxhdpi-v21',
+    ),
+    'xxxhdpi': (
+        'xxxhdpi-v4',
+        'ldrtl-xxxhdpi-v4',
+        'sw600dp-xxxhdpi-v13',
+        'ldrtl-xxxhdpi-v17',
+        'ldrtl-sw600dp-xxxhdpi-v17',
+        'xxxhdpi-v21',
+    ),
+    'tvdpi': (
+        'tvdpi-v4',
+        'sw600dp-tvdpi-v13',
+        'ldrtl-sw600dp-tvdpi-v17',
+    ),
+}
+
+
+
+
 def _ParseArgs(args):
   """Parses command line options.
 
@@ -63,6 +132,7 @@
       help='Make a resource package that can be loaded as shared library.')
 
   parser.add_option('--resource-dirs',
+                    default='[]',
                     help='Directories containing resources of this target.')
   parser.add_option('--dependencies-res-zips',
                     help='Resources from dependents.')
@@ -97,21 +167,44 @@
       help='For each additional package, the R.txt file should contain a '
       'list of resources to be included in the R.java file in the format '
       'generated by aapt')
-  parser.add_option(
-      '--include-all-resources',
-      action='store_true',
-      help='Include every resource ID in every generated R.java file '
-      '(ignoring R.txt).')
 
-  parser.add_option(
-      '--all-resources-zip-out',
-      help='Path for output of all resources. This includes resources in '
-      'dependencies.')
   parser.add_option('--support-zh-hk', action='store_true',
                     help='Use zh-rTW resources for zh-rHK.')
 
   parser.add_option('--stamp', help='File to touch on success')
 
+  parser.add_option('--debuggable',
+                    action='store_true',
+                    help='Whether to add android:debuggable="true"')
+  parser.add_option('--version-code', help='Version code for apk.')
+  parser.add_option('--version-name', help='Version name for apk.')
+  parser.add_option('--no-compress', help='disables compression for the '
+                    'given comma separated list of extensions')
+  parser.add_option(
+      '--create-density-splits',
+      action='store_true',
+      help='Enables density splits')
+  parser.add_option('--language-splits',
+                    default='[]',
+                    help='GN list of languages to create splits for')
+  parser.add_option('--locale-whitelist',
+                    default='[]',
+                    help='GN list of languages to include. All other language '
+                         'configs will be stripped out. List may include '
+                         'a combination of Android locales or Chrome locales.')
+  parser.add_option('--apk-path',
+                    help='Path to output (partial) apk.')
+  parser.add_option('--exclude-xxxhdpi', action='store_true',
+                    help='Do not include xxxhdpi drawables.')
+  parser.add_option('--xxxhdpi-whitelist',
+                    default='[]',
+                    help='GN list of globs that say which xxxhdpi images to '
+                         'include even when --exclude-xxxhdpi is set.')
+  parser.add_option('--png-to-webp', action='store_true',
+                    help='Convert png files to webp format.')
+  parser.add_option('--webp-binary', default='',
+                    help='Path to the cwebp binary.')
+
   options, positional_args = parser.parse_args(args)
 
   if positional_args:
@@ -123,7 +216,6 @@
       'aapt_path',
       'android_manifest',
       'dependencies_res_zips',
-      'resource_dirs',
       )
   build_utils.CheckOptions(options, parser, required=required_options)
 
@@ -131,6 +223,10 @@
   options.dependencies_res_zips = (
       build_utils.ParseGnList(options.dependencies_res_zips))
 
+  options.language_splits = build_utils.ParseGnList(options.language_splits)
+  options.locale_whitelist = build_utils.ParseGnList(options.locale_whitelist)
+  options.xxxhdpi_whitelist = build_utils.ParseGnList(options.xxxhdpi_whitelist)
+
   # Don't use [] as default value since some script explicitly pass "".
   if options.extra_res_packages:
     options.extra_res_packages = (
@@ -147,7 +243,7 @@
   return options
 
 
-def CreateRJavaFiles(srcjar_dir, main_r_txt_file, packages, r_txt_files,
+def _CreateRJavaFiles(srcjar_dir, main_r_txt_file, packages, r_txt_files,
                      shared_resources, non_constant_id):
   assert len(packages) == len(r_txt_files), 'Need one R.txt file per package'
 
@@ -155,6 +251,7 @@
   # Contains the correct values for resources.
   all_resources = {}
   for entry in _ParseTextSymbolsFile(main_r_txt_file):
+    entry = entry._replace(value=_FixPackageIds(entry.value))
     all_resources[(entry.resource_type, entry.name)] = entry
 
   # Map of package_name->resource_type->entry
@@ -214,6 +311,18 @@
   return ret
 
 
+def _FixPackageIds(resource_value):
+  # Resource IDs for resources belonging to regular APKs have their first byte
+  # as 0x7f (package id). However with webview, since it is not a regular apk
+  # but used as a shared library, aapt is passed the --shared-resources flag
+  # which changes some of the package ids to 0x02 and 0x00.  This function just
+  # normalises all package ids to 0x7f, which the generated code in R.java
+  # changes to the correct package id at runtime.
+  # resource_value is a string with either, a single value '0x12345678', or an
+  # array of values like '{ 0xfedcba98, 0x01234567, 0x56789abc }'
+  return re.sub(r'0x(?!01)\d\d', r'0x7f', resource_value)
+
+
 def _CreateRJavaFile(package, resources_by_type, shared_resources,
                      non_constant_id):
   """Generates the contents of a R.java file."""
@@ -277,7 +386,7 @@
                          final=final)
 
 
-def CrunchDirectory(aapt, input_dir, output_dir):
+def _CrunchDirectory(aapt, input_dir, output_dir):
   """Crunches the images in input_dir and its subdirectories into output_dir.
 
   If an image is already optimized, crunching often increases image size. In
@@ -288,8 +397,8 @@
               '-C', output_dir,
               '-S', input_dir,
               '--ignore-assets', build_utils.AAPT_IGNORE_PATTERN]
-  build_utils.CheckOutput(aapt_cmd, stderr_filter=FilterCrunchStderr,
-                          fail_func=DidCrunchFail)
+  build_utils.CheckOutput(aapt_cmd, stderr_filter=_FilterCrunchStderr,
+                          fail_func=_DidCrunchFail)
 
   # Check for images whose size increased during crunching and replace them
   # with their originals (except for 9-patches, which must be crunched).
@@ -307,7 +416,7 @@
         shutil.copyfile(original, crunched)
 
 
-def FilterCrunchStderr(stderr):
+def _FilterCrunchStderr(stderr):
   """Filters out lines from aapt crunch's stderr that can safely be ignored."""
   filtered_lines = []
   for line in stderr.splitlines(True):
@@ -320,7 +429,7 @@
   return ''.join(filtered_lines)
 
 
-def DidCrunchFail(returncode, stderr):
+def _DidCrunchFail(returncode, stderr):
   """Determines whether aapt crunch failed from its return code and output.
 
   Because aapt's return code cannot be trusted, any output to stderr is
@@ -329,7 +438,7 @@
   return returncode != 0 or stderr
 
 
-def ZipResources(resource_dirs, zip_path):
+def _ZipResources(resource_dirs, zip_path):
   # Python zipfile does not provide a way to replace a file (it just writes
   # another file with the same name). So, first collect all the files to put
   # in the zip (with proper overriding), and then zip them.
@@ -346,103 +455,325 @@
   build_utils.DoZip(files_to_zip.iteritems(), zip_path)
 
 
-def CombineZips(zip_files, output_path, support_zh_hk):
-  # When packaging resources, if the top-level directories in the zip file are
-  # of the form 0, 1, ..., then each subdirectory will be passed to aapt as a
-  # resources directory. While some resources just clobber others (image files,
-  # etc), other resources (particularly .xml files) need to be more
-  # intelligently merged. That merging is left up to aapt.
-  def path_transform(name, src_zip):
-    return '%d/%s' % (zip_files.index(src_zip), name)
-
-  # We don't currently support zh-HK on Chrome for Android, but on the
-  # native side we resolve zh-HK resources to zh-TW. This logic is
-  # duplicated here by just copying the zh-TW res folders to zh-HK.
-  # See https://crbug.com/780847.
-  with build_utils.TempDir() as temp_dir:
-    if support_zh_hk:
-      zip_files = _DuplicateZhResources(zip_files, temp_dir)
-    build_utils.MergeZips(output_path, zip_files, path_transform=path_transform)
-
-
-def _DuplicateZhResources(zip_files, temp_dir):
-  new_zip_files = []
-  for i, zip_path in enumerate(zip_files):
-    # We use zh-TW resources for zh-HK (if we have zh-TW resources). If no
-    # zh-TW resources exists (ex. api specific resources), then just use the
-    # original zip.
-    if not _ZipContains(zip_path, r'zh-r(HK|TW)'):
-      new_zip_files.append(zip_path)
-      continue
-
-    resource_dir = os.path.join(temp_dir, str(i))
-    new_zip_path = os.path.join(temp_dir, str(i) + '.zip')
-
-    # Exclude existing zh-HK resources so that we don't mess up any resource
-    # IDs. This can happen if the type IDs in the existing resources don't
-    # align with ours (since they've already been generated at this point).
-    build_utils.ExtractAll(
-        zip_path, path=resource_dir, predicate=lambda x: not 'zh-rHK' in x)
+def _DuplicateZhResources(resource_dirs):
+  for resource_dir in resource_dirs:
+    # We use zh-TW resources for zh-HK (if we have zh-TW resources).
     for path in build_utils.IterFiles(resource_dir):
       if 'zh-rTW' in path:
         hk_path = path.replace('zh-rTW', 'zh-rHK')
-        build_utils.Touch(hk_path)
+        build_utils.MakeDirectory(os.path.dirname(hk_path))
         shutil.copyfile(path, hk_path)
 
-    build_utils.ZipDir(new_zip_path, resource_dir)
-    new_zip_files.append(new_zip_path)
-  return new_zip_files
-
-
-def _ZipContains(path, pattern):
-  with zipfile.ZipFile(path, 'r') as z:
-    return any(re.search(pattern, f) for f in z.namelist())
-
-
 def _ExtractPackageFromManifest(manifest_path):
   doc = xml.etree.ElementTree.parse(manifest_path)
   return doc.getroot().get('package')
 
 
+def _ToAaptLocales(locale_whitelist, support_zh_hk):
+  """Converts the list of Chrome locales to aapt config locales."""
+  ret = set()
+  for locale in locale_whitelist:
+    locale = _CHROME_TO_ANDROID_LOCALE_MAP.get(locale, locale)
+    if locale is None or ('-' in locale and '-r' not in locale):
+      raise Exception('_CHROME_TO_ANDROID_LOCALE_MAP needs updating.'
+                      ' Found: %s' % locale)
+    ret.add(locale)
+    # Always keep non-regional fall-backs.
+    language = locale.split('-')[0]
+    ret.add(language)
+
+  # We don't actually support zh-HK in Chrome on Android, but we mimic the
+  # native side behavior where we use zh-TW resources when the locale is set to
+  # zh-HK. See https://crbug.com/780847.
+  if support_zh_hk:
+    assert not any('HK' in l for l in locale_whitelist), (
+        'Remove special logic if zh-HK is now supported (crbug.com/780847).')
+    ret.add('zh-rHK')
+  return sorted(ret)
+
+
+def _MoveImagesToNonMdpiFolders(res_root):
+  """Move images from drawable-*-mdpi-* folders to drawable-* folders.
+
+  Why? http://crbug.com/289843
+  """
+  for src_dir_name in os.listdir(res_root):
+    src_components = src_dir_name.split('-')
+    if src_components[0] != 'drawable' or 'mdpi' not in src_components:
+      continue
+    src_dir = os.path.join(res_root, src_dir_name)
+    if not os.path.isdir(src_dir):
+      continue
+    dst_components = [c for c in src_components if c != 'mdpi']
+    assert dst_components != src_components
+    dst_dir_name = '-'.join(dst_components)
+    dst_dir = os.path.join(res_root, dst_dir_name)
+    build_utils.MakeDirectory(dst_dir)
+    for src_file_name in os.listdir(src_dir):
+      if not src_file_name.endswith('.png'):
+        continue
+      src_file = os.path.join(src_dir, src_file_name)
+      dst_file = os.path.join(dst_dir, src_file_name)
+      assert not os.path.lexists(dst_file)
+      shutil.move(src_file, dst_file)
+
+
+def _GenerateDensitySplitPaths(apk_path):
+  for density, config in _DENSITY_SPLITS.iteritems():
+    src_path = '%s_%s' % (apk_path, '_'.join(config))
+    dst_path = '%s_%s' % (apk_path, density)
+    yield src_path, dst_path
+
+
+def _GenerateLanguageSplitOutputPaths(apk_path, languages):
+  for lang in languages:
+    yield '%s_%s' % (apk_path, lang)
+
+
+def _RenameDensitySplits(apk_path):
+  """Renames all density splits to have shorter / predictable names."""
+  for src_path, dst_path in _GenerateDensitySplitPaths(apk_path):
+    shutil.move(src_path, dst_path)
+
+
+def _CheckForMissedConfigs(apk_path, check_density, languages):
+  """Raises an exception if apk_path contains any unexpected configs."""
+  triggers = []
+  if check_density:
+    triggers.extend(re.compile('-%s' % density) for density in _DENSITY_SPLITS)
+  if languages:
+    triggers.extend(re.compile(r'-%s\b' % lang) for lang in languages)
+  with zipfile.ZipFile(apk_path) as main_apk_zip:
+    for name in main_apk_zip.namelist():
+      for trigger in triggers:
+        if trigger.search(name) and not 'mipmap-' in name:
+          raise Exception(('Found config in main apk that should have been ' +
+                           'put into a split: %s\nYou need to update ' +
+                           'package_resources.py to include this new ' +
+                           'config (trigger=%s)') % (name, trigger.pattern))
+
+
+def _CreatePackageApkArgs(options):
+  package_command = [
+    '--version-code', options.version_code,
+    '--version-name', options.version_name,
+    '-f',
+    '-F', options.apk_path,
+  ]
+
+  if options.proguard_file:
+    package_command += ['-G', options.proguard_file]
+  if options.proguard_file_main_dex:
+    package_command += ['-D', options.proguard_file_main_dex]
+
+  if options.no_compress:
+    for ext in options.no_compress.split(','):
+      package_command += ['-0', ext]
+
+  if options.shared_resources:
+    package_command.append('--shared-lib')
+  if options.app_as_shared_lib:
+    package_command.append('--app-as-shared-lib')
+
+  if options.create_density_splits:
+    for config in _DENSITY_SPLITS.itervalues():
+      package_command.extend(('--split', ','.join(config)))
+
+  if options.language_splits:
+    for lang in options.language_splits:
+      package_command.extend(('--split', lang))
+
+  if options.debuggable:
+    package_command += ['--debug-mode']
+
+  if options.locale_whitelist:
+    aapt_locales = _ToAaptLocales(
+        options.locale_whitelist, options.support_zh_hk)
+    package_command += ['-c', ','.join(aapt_locales)]
+
+  return package_command
+
+
+def _ResourceNameFromPath(path):
+  return os.path.splitext(os.path.basename(path))[0]
+
+
+def _CreateKeepPredicate(resource_dirs, exclude_xxxhdpi, xxxhdpi_whitelist):
+  if not exclude_xxxhdpi:
+    # Do not extract dotfiles (e.g. ".gitkeep"). aapt ignores them anyways.
+    return lambda path: os.path.basename(path)[0] != '.'
+
+  # Returns False only for xxxhdpi non-mipmap, non-whitelisted drawables.
+  naive_predicate = lambda path: (
+      not re.search(r'[/-]xxxhdpi[/-]', path) or
+      re.search(r'[/-]mipmap[/-]', path) or
+      build_utils.MatchesGlob(path, xxxhdpi_whitelist))
+
+  # Build a set of all non-xxxhdpi drawables to ensure that we never exclude any
+  # xxxhdpi drawable that does not exist in other densities.
+  non_xxxhdpi_drawables = set()
+  for resource_dir in resource_dirs:
+    for path in build_utils.IterFiles(resource_dir):
+      if re.search(r'[/-]drawable[/-]', path) and naive_predicate(path):
+        non_xxxhdpi_drawables.add(_ResourceNameFromPath(path))
+
+  return lambda path: (naive_predicate(path) or
+                       _ResourceNameFromPath(path) not in non_xxxhdpi_drawables)
+
+
+def _ConvertToWebP(webp_binary, png_files):
+  pool = multiprocessing.pool.ThreadPool(10)
+  def convert_image(png_path):
+    root = os.path.splitext(png_path)[0]
+    webp_path = root + '.webp'
+    args = [webp_binary, png_path, '-mt', '-quiet', '-m', '6', '-q', '100',
+        '-lossless', '-o', webp_path]
+    subprocess.check_call(args)
+    os.remove(png_path)
+  # Android requires pngs for 9-patch images.
+  pool.map(convert_image, [f for f in png_files if not f.endswith('.9.png')])
+  pool.close()
+  pool.join()
+
+
+def _PackageApk(options, package_command, dep_subdirs):
+  _DuplicateZhResources(dep_subdirs)
+
+  package_command += _CreatePackageApkArgs(options)
+
+  keep_predicate = _CreateKeepPredicate(
+      dep_subdirs, options.exclude_xxxhdpi, options.xxxhdpi_whitelist)
+  png_paths = []
+  for directory in dep_subdirs:
+    for f in build_utils.IterFiles(directory):
+      if not keep_predicate(f):
+        os.remove(f)
+      elif f.endswith('.png'):
+        png_paths.append(f)
+  if png_paths and options.png_to_webp:
+    _ConvertToWebP(options.webp_binary, png_paths)
+  for directory in dep_subdirs:
+    _MoveImagesToNonMdpiFolders(directory)
+
+  # Creates a .zip with AndroidManifest.xml, resources.arsc, res/*
+  # Also creates R.txt
+  build_utils.CheckOutput(
+      package_command, print_stdout=False, print_stderr=False)
+
+  if options.create_density_splits or options.language_splits:
+    _CheckForMissedConfigs(options.apk_path, options.create_density_splits,
+                          options.language_splits)
+
+  if options.create_density_splits:
+    _RenameDensitySplits(options.apk_path)
+
+
+def _PackageLibrary(options, package_command, temp_dir):
+  v14_dir = os.path.join(temp_dir, 'v14')
+  build_utils.MakeDirectory(v14_dir)
+
+  input_resource_dirs = options.resource_dirs
+
+  for d in input_resource_dirs:
+    package_command += ['-S', d]
+
+  if not options.v14_skip:
+    for resource_dir in input_resource_dirs:
+      generate_v14_compatible_resources.GenerateV14Resources(
+          resource_dir,
+          v14_dir)
+
+  # This is the list of directories with resources to put in the final .zip
+  # file. The order of these is important so that crunched/v14 resources
+  # override the normal ones.
+  zip_resource_dirs = input_resource_dirs + [v14_dir]
+
+  base_crunch_dir = os.path.join(temp_dir, 'crunch')
+  # Crunch image resources. This shrinks png files and is necessary for
+  # 9-patch images to display correctly. 'aapt crunch' accepts only a single
+  # directory at a time and deletes everything in the output directory.
+  for idx, input_dir in enumerate(input_resource_dirs):
+    crunch_dir = os.path.join(base_crunch_dir, str(idx))
+    build_utils.MakeDirectory(crunch_dir)
+    zip_resource_dirs.append(crunch_dir)
+    _CrunchDirectory(options.aapt_path, input_dir, crunch_dir)
+
+  if options.resource_zip_out:
+    _ZipResources(zip_resource_dirs, options.resource_zip_out)
+
+  # Only creates an R.txt
+  build_utils.CheckOutput(
+      package_command, print_stdout=False, print_stderr=False)
+
+
+def _CreateRTxtAndSrcJar(options, r_txt_path, srcjar_dir):
+  # When an empty res/ directory is passed, aapt does not write an R.txt.
+  if not os.path.exists(r_txt_path):
+    build_utils.Touch(r_txt_path)
+
+  if options.r_text_in:
+    r_txt_path = options.r_text_in
+
+  packages = list(options.extra_res_packages)
+  r_txt_files = list(options.extra_r_text_files)
+
+  cur_package = options.custom_package
+  if not options.custom_package:
+    cur_package = _ExtractPackageFromManifest(options.android_manifest)
+
+  # Don't create a .java file for the current resource target when:
+  # - no package name was provided (either by manifest or build rules),
+  # - there was already a dependent android_resources() with the same
+  #   package (occurs mostly when an apk target and resources target share
+  #   an AndroidManifest.xml)
+  if cur_package != 'org.dummy' and cur_package not in packages:
+    packages.append(cur_package)
+    r_txt_files.append(r_txt_path)
+
+  if packages:
+    shared_resources = options.shared_resources or options.app_as_shared_lib
+    _CreateRJavaFiles(srcjar_dir, r_txt_path, packages, r_txt_files,
+                     shared_resources, options.non_constant_id)
+
+  if options.srcjar_out:
+    build_utils.ZipDir(options.srcjar_out, srcjar_dir)
+
+  if options.r_text_out:
+    shutil.copyfile(r_txt_path, options.r_text_out)
+
+
+def _ExtractDeps(dep_zips, deps_dir):
+  dep_subdirs = []
+  for z in dep_zips:
+    subdir = os.path.join(deps_dir, os.path.basename(z))
+    if os.path.exists(subdir):
+      raise Exception('Resource zip name conflict: ' + os.path.basename(z))
+    build_utils.ExtractAll(z, path=subdir)
+    dep_subdirs.append(subdir)
+  return dep_subdirs
+
+
 def _OnStaleMd5(options):
-  aapt = options.aapt_path
   with build_utils.TempDir() as temp_dir:
     deps_dir = os.path.join(temp_dir, 'deps')
     build_utils.MakeDirectory(deps_dir)
-    v14_dir = os.path.join(temp_dir, 'v14')
-    build_utils.MakeDirectory(v14_dir)
-
     gen_dir = os.path.join(temp_dir, 'gen')
     build_utils.MakeDirectory(gen_dir)
     r_txt_path = os.path.join(gen_dir, 'R.txt')
     srcjar_dir = os.path.join(temp_dir, 'java')
 
-    input_resource_dirs = options.resource_dirs
-
-    if not options.v14_skip:
-      for resource_dir in input_resource_dirs:
-        generate_v14_compatible_resources.GenerateV14Resources(
-            resource_dir,
-            v14_dir)
-
-    dep_zips = options.dependencies_res_zips
-    dep_subdirs = []
-    for z in dep_zips:
-      subdir = os.path.join(deps_dir, os.path.basename(z))
-      if os.path.exists(subdir):
-        raise Exception('Resource zip name conflict: ' + os.path.basename(z))
-      build_utils.ExtractAll(z, path=subdir)
-      dep_subdirs.append(subdir)
+    dep_subdirs = _ExtractDeps(options.dependencies_res_zips, deps_dir)
 
     # Generate R.java. This R.java contains non-final constants and is used only
     # while compiling the library jar (e.g. chromium_content.jar). When building
     # an apk, a new R.java file with the correct resource -> ID mappings will be
     # generated by merging the resources from all libraries and the main apk
     # project.
-    package_command = [aapt,
+    package_command = [options.aapt_path,
                        'package',
                        '-m',
                        '-M', options.android_manifest,
+                       '--no-crunch',
                        '--auto-add-overlay',
                        '--no-version-vectors',
                        '-I', options.android_sdk_jar,
@@ -450,31 +781,6 @@
                        '-J', gen_dir,  # Required for R.txt generation.
                        '--ignore-assets', build_utils.AAPT_IGNORE_PATTERN]
 
-    # aapt supports only the "--include-all-resources" mode, where each R.java
-    # file ends up with all symbols, rather than only those that it had at the
-    # time it was originally generated. This subtle difference makes no
-    # difference when compiling, but can lead to increased unused symbols in the
-    # resulting R.class files.
-    # TODO(agrieve): See if proguard makes this difference actually translate
-    # into a size difference. If not, we can delete all of our custom R.java
-    # template code above (and make include_all_resources the default).
-    if options.include_all_resources:
-      srcjar_dir = gen_dir
-      if options.extra_res_packages:
-        colon_separated = ':'.join(options.extra_res_packages)
-        package_command += ['--extra-packages', colon_separated]
-      if options.non_constant_id:
-        package_command.append('--non-constant-id')
-      if options.custom_package:
-        package_command += ['--custom-package', options.custom_package]
-      if options.shared_resources:
-        package_command.append('--shared-lib')
-      if options.app_as_shared_lib:
-        package_command.append('--app-as-shared-lib')
-
-    for d in input_resource_dirs:
-      package_command += ['-S', d]
-
     # Adding all dependencies as sources is necessary for @type/foo references
     # to symbols within dependencies to resolve. However, it has the side-effect
     # that all Java symbols from dependencies are copied into the new R.java.
@@ -484,74 +790,12 @@
     for d in dep_subdirs:
       package_command += ['-S', d]
 
-    if options.proguard_file:
-      package_command += ['-G', options.proguard_file]
-    if options.proguard_file_main_dex:
-      package_command += ['-D', options.proguard_file_main_dex]
-    build_utils.CheckOutput(package_command, print_stderr=False)
+    if options.apk_path:
+      _PackageApk(options, package_command, dep_subdirs)
+    else:
+      _PackageLibrary(options, package_command, temp_dir)
 
-    # When an empty res/ directory is passed, aapt does not write an R.txt.
-    if not os.path.exists(r_txt_path):
-      build_utils.Touch(r_txt_path)
-
-    if not options.include_all_resources:
-      # --include-all-resources can only be specified for generating final R
-      # classes for APK. It makes no sense for APK to have pre-generated R.txt
-      # though, because aapt-generated already lists all available resources.
-      if options.r_text_in:
-        r_txt_path = options.r_text_in
-
-      packages = list(options.extra_res_packages)
-      r_txt_files = list(options.extra_r_text_files)
-
-      cur_package = options.custom_package
-      if not options.custom_package:
-        cur_package = _ExtractPackageFromManifest(options.android_manifest)
-
-      # Don't create a .java file for the current resource target when:
-      # - no package name was provided (either by manifest or build rules),
-      # - there was already a dependent android_resources() with the same
-      #   package (occurs mostly when an apk target and resources target share
-      #   an AndroidManifest.xml)
-      if cur_package != 'org.dummy' and cur_package not in packages:
-        packages.append(cur_package)
-        r_txt_files.append(r_txt_path)
-
-      if packages:
-        shared_resources = options.shared_resources or options.app_as_shared_lib
-        CreateRJavaFiles(srcjar_dir, r_txt_path, packages, r_txt_files,
-                         shared_resources, options.non_constant_id)
-
-    # This is the list of directories with resources to put in the final .zip
-    # file. The order of these is important so that crunched/v14 resources
-    # override the normal ones.
-    zip_resource_dirs = input_resource_dirs + [v14_dir]
-
-    base_crunch_dir = os.path.join(temp_dir, 'crunch')
-
-    # Crunch image resources. This shrinks png files and is necessary for
-    # 9-patch images to display correctly. 'aapt crunch' accepts only a single
-    # directory at a time and deletes everything in the output directory.
-    for idx, input_dir in enumerate(input_resource_dirs):
-      crunch_dir = os.path.join(base_crunch_dir, str(idx))
-      build_utils.MakeDirectory(crunch_dir)
-      zip_resource_dirs.append(crunch_dir)
-      CrunchDirectory(aapt, input_dir, crunch_dir)
-
-    if options.resource_zip_out:
-      ZipResources(zip_resource_dirs, options.resource_zip_out)
-
-    if options.all_resources_zip_out:
-      all_zips = [options.resource_zip_out] if options.resource_zip_out else []
-      all_zips += dep_zips
-      CombineZips(all_zips,
-                  options.all_resources_zip_out, options.support_zh_hk)
-
-    if options.srcjar_out:
-      build_utils.ZipDir(options.srcjar_out, srcjar_dir)
-
-    if options.r_text_out:
-      shutil.copyfile(r_txt_path, options.r_text_out)
+    _CreateRTxtAndSrcJar(options, r_txt_path, srcjar_dir)
 
 
 def main(args):
@@ -561,8 +805,8 @@
   # Order of these must match order specified in GN so that the correct one
   # appears first in the depfile.
   possible_output_paths = [
+    options.apk_path,
     options.resource_zip_out,
-    options.all_resources_zip_out,
     options.r_text_out,
     options.srcjar_out,
     options.proguard_file,
@@ -570,18 +814,30 @@
   ]
   output_paths = [x for x in possible_output_paths if x]
 
+  if options.apk_path and options.create_density_splits:
+    for _, dst_path in _GenerateDensitySplitPaths(options.apk_path):
+      output_paths.append(dst_path)
+  if options.apk_path and options.language_splits:
+    output_paths.extend(
+        _GenerateLanguageSplitOutputPaths(options.apk_path,
+                                          options.language_splits))
+
   # List python deps in input_strings rather than input_paths since the contents
   # of them does not change what gets written to the depsfile.
   input_strings = options.extra_res_packages + [
     options.app_as_shared_lib,
     options.custom_package,
-    options.include_all_resources,
     options.non_constant_id,
     options.shared_resources,
     options.v14_skip,
+    options.exclude_xxxhdpi,
+    options.xxxhdpi_whitelist,
+    str(options.png_to_webp),
+    str(options.support_zh_hk),
   ]
-  if options.support_zh_hk:
-    input_strings.append('support_zh_hk')
+
+  if options.apk_path:
+    input_strings.extend(_CreatePackageApkArgs(options))
 
   input_paths = [
     options.aapt_path,
diff --git a/build/config/android/internal_rules.gni b/build/config/android/internal_rules.gni
index cd2941d..daa081f 100644
--- a/build/config/android/internal_rules.gni
+++ b/build/config/android/internal_rules.gni
@@ -1383,7 +1383,19 @@
 
   # Runs process_resources.py
   template("process_resources") {
-    action(target_name) {
+    _process_resources_target_name = target_name
+    if (defined(invoker.output)) {
+      _post_process = defined(invoker.post_process_script)
+      _packaged_resources_path = invoker.output
+      if (_post_process) {
+        _process_resources_target_name = "${target_name}__intermediate"
+        _packaged_resources_path =
+            get_path_info(_packaged_resources_path, "dir") + "/" +
+            get_path_info(_packaged_resources_path, "name") +
+            ".intermediate.ap_"
+      }
+    }
+    action(_process_resources_target_name) {
       set_sources_assignment_filter([])
       forward_variables_from(invoker,
                              [
@@ -1426,6 +1438,7 @@
       }
 
       inputs = [
+        android_default_aapt_path,
         invoker.build_config,
         invoker.android_manifest,
         _android_aapt_path,
@@ -1445,12 +1458,41 @@
         rebase_path(_android_aapt_path, root_build_dir),
         "--android-manifest",
         rebase_path(invoker.android_manifest, root_build_dir),
-        "--resource-dirs=$_rebased_all_resource_dirs",
         "--dependencies-res-zips=@FileArg($_rebased_build_config:resources:dependency_zips)",
         "--extra-res-packages=@FileArg($_rebased_build_config:resources:extra_package_names)",
         "--extra-r-text-files=@FileArg($_rebased_build_config:resources:extra_r_text_files)",
       ]
 
+      if (_rebased_all_resource_dirs != []) {
+        args += [ "--resource-dirs=$_rebased_all_resource_dirs" ]
+      }
+
+      if (defined(invoker.version_code)) {
+        args += [
+          "--version-code",
+          invoker.version_code,
+        ]
+      }
+      if (defined(invoker.version_name)) {
+        args += [
+          "--version-name",
+          invoker.version_name,
+        ]
+      }
+      if (defined(_packaged_resources_path)) {
+        outputs += [ _packaged_resources_path ]
+        args += [
+          "--apk-path",
+          rebase_path(_packaged_resources_path, root_build_dir),
+        ]
+      }
+
+      # Useful to have android:debuggable in the manifest even for Release
+      # builds. Just omit it for officai
+      if (debuggable_apks) {
+        args += [ "--debuggable" ]
+      }
+
       if (defined(invoker.zip_path)) {
         outputs += [ invoker.zip_path ]
         args += [
@@ -1459,15 +1501,6 @@
         ]
       }
 
-      if (defined(invoker.all_resources_zip_path)) {
-        _all_resources_zip = invoker.all_resources_zip_path
-        outputs += [ _all_resources_zip ]
-        args += [
-          "--all-resources-zip-out",
-          rebase_path(_all_resources_zip, root_build_dir),
-        ]
-      }
-
       if (defined(invoker.r_text_out_path)) {
         outputs += [ invoker.r_text_out_path ]
         args += [
@@ -1517,11 +1550,6 @@
         args += [ "--app-as-shared-lib" ]
       }
 
-      if (defined(invoker.include_all_resources) &&
-          invoker.include_all_resources) {
-        args += [ "--include-all-resources" ]
-      }
-
       if (defined(invoker.proguard_file)) {
         outputs += [ invoker.proguard_file ]
         args += [
@@ -1538,112 +1566,6 @@
         ]
       }
 
-      if (defined(invoker.support_zh_hk) && invoker.support_zh_hk) {
-        args += [ "--support-zh-hk" ]
-      }
-
-      if (defined(invoker.args)) {
-        args += invoker.args
-      }
-    }
-  }
-
-  # Runs aapt to create an .ap_ file, which is a zip file containing
-  # compiled xml and a resources.arsc file.
-  #
-  # Required Variables:
-  #   output: Path to .ap_ to create.
-  #   android_manifest: The AndroidManifest.xml for the apk.
-  #   version_code: The verison code to use.
-  #   version_name: The verison name to use.
-  # Optional Variables:
-  #   aapt_locale_whitelist: If set, all locales not in this list will be
-  #     stripped from resources.arsc.
-  #   alternative_android_sdk_jar: An alternative android sdk jar.
-  #   app_as_shared_lib: Enables --app-as-shared-lib.
-  #   exclude_xxxhdpi: Causes all drawable-xxxhdpi images to be excluded
-  #     (mipmaps are still included).
-  #   png_to_webp: If true, pngs (with the exception of 9-patch) are
-  #     converted to webp.
-  #   post_process_script: Script to call to post-process the .ap_.
-  #   resources_zip: Resource .zip file created by process_resources to package.
-  #   shared_resources: Enables --shared-lib.
-  #   density_splits: A list of densities to create apk splits for.
-  #   language_splits: A list of language codes to create apk splits for.
-  #   xxxhdpi_whitelist: A list of globs used when exclude_xxxhdpi=true. Files
-  #     that match this whitelist will still be included.
-  template("package_resources") {
-    _post_process = defined(invoker.post_process_script)
-    _package_resources_target_name = target_name
-    _packaged_resources_path = invoker.output
-    if (_post_process) {
-      _package_resources_target_name = "${target_name}__intermediate"
-      _packaged_resources_path =
-          get_path_info(_packaged_resources_path, "dir") + "/" +
-          get_path_info(_packaged_resources_path, "name") + ".intermediate.ap_"
-    }
-
-    action(_package_resources_target_name) {
-      forward_variables_from(invoker,
-                             [
-                               "deps",
-                               "testonly",
-                               "visibility",
-                             ])
-
-      script = "//build/android/gyp/package_resources.py"
-      depfile = "${target_gen_dir}/${target_name}.d"
-      outputs = [
-        _packaged_resources_path,
-      ]
-
-      _android_sdk_jar = android_sdk_jar
-      if (defined(invoker.alternative_android_sdk_jar)) {
-        _android_sdk_jar = invoker.alternative_android_sdk_jar
-      }
-
-      inputs = [
-        android_default_aapt_path,
-        _android_sdk_jar,
-        invoker.android_manifest,
-      ]
-
-      args = [
-        "--depfile",
-        rebase_path(depfile, root_build_dir),
-        "--android-sdk-jar",
-        rebase_path(_android_sdk_jar, root_build_dir),
-        "--aapt-path",
-        rebase_path(android_default_aapt_path, root_build_dir),
-        "--android-manifest",
-        rebase_path(invoker.android_manifest, root_build_dir),
-        "--version-code",
-        invoker.version_code,
-        "--version-name",
-        invoker.version_name,
-        "--apk-path",
-        rebase_path(_packaged_resources_path, root_build_dir),
-      ]
-
-      # Useful to have android:debuggable in the manifest even for Release
-      # builds. Just omit it for officai
-      if (debuggable_apks) {
-        args += [ "--debuggable" ]
-      }
-
-      if (defined(invoker.resources_zip)) {
-        inputs += [ invoker.resources_zip ]
-        args += [
-          "--resource-zips",
-          rebase_path(invoker.resources_zip, root_build_dir),
-        ]
-      }
-      if (defined(invoker.shared_resources) && invoker.shared_resources) {
-        args += [ "--shared-resources" ]
-      }
-      if (defined(invoker.app_as_shared_lib) && invoker.app_as_shared_lib) {
-        args += [ "--app-as-shared-lib" ]
-      }
       if (defined(invoker.density_splits) && invoker.density_splits != []) {
         args += [ "--create-density-splits" ]
         foreach(_density, invoker.density_splits) {
@@ -1675,12 +1597,17 @@
           args += [ "--xxxhdpi-whitelist=${invoker.xxxhdpi_whitelist}" ]
         }
       }
+
       if (defined(invoker.support_zh_hk) && invoker.support_zh_hk) {
         args += [ "--support-zh-hk" ]
       }
+
+      if (defined(invoker.args)) {
+        args += invoker.args
+      }
     }
 
-    if (_post_process) {
+    if (defined(_packaged_resources_path) && _post_process) {
       action(target_name) {
         depfile = "${target_gen_dir}/${target_name}.d"
         script = invoker.post_process_script
@@ -1699,7 +1626,7 @@
           invoker.output,
         ]
         public_deps = [
-          ":${_package_resources_target_name}",
+          ":${_process_resources_target_name}",
         ]
       }
     }
diff --git a/build/config/android/rules.gni b/build/config/android/rules.gni
index 62d500f..753d974 100644
--- a/build/config/android/rules.gni
+++ b/build/config/android/rules.gni
@@ -1740,8 +1740,6 @@
   #     (optional).
   #   apk_under_test: For an instrumentation test apk, this is the target of the
   #     tested apk.
-  #   include_all_resources - If true include all resource IDs in all generated
-  #     R.java files.
   #   testonly: Marks this target as "test-only".
   #   write_asset_list: Adds an extra file to the assets, which contains a list of
   #     all other asset files.
@@ -1794,7 +1792,6 @@
 
     # JUnit tests use resource zip files. These must not be put in gen/
     # directory or they will not be available to tester bots.
-    _all_resources_zip_path = "$_base_path.resources.all.zip"
     _jar_path = "$_base_path.jar"
     _lib_dex_path = "$_base_path.dex.jar"
     _rebased_lib_dex_path = rebase_path(_lib_dex_path, root_build_dir)
@@ -2113,19 +2110,29 @@
                              [
                                "alternative_android_sdk_jar",
                                "app_as_shared_lib",
-                               "include_all_resources",
                                "shared_resources",
                                "support_zh_hk",
+                               "aapt_locale_whitelist",
+                               "exclude_xxxhdpi",
+                               "png_to_webp",
+                               "xxxhdpi_whitelist",
                              ])
+      android_manifest = _android_manifest
+      version_code = _version_code
+      version_name = _version_name
+      if (defined(invoker.post_process_package_resources_script)) {
+        post_process_script = invoker.post_process_package_resources_script
+      }
       srcjar_path = "${target_gen_dir}/${target_name}.srcjar"
       r_text_out_path = "${target_gen_dir}/${target_name}_R.txt"
-      android_manifest = _android_manifest
-      all_resources_zip_path = _all_resources_zip_path
       generate_constant_ids = true
       proguard_file = _generated_proguard_config
       if (_enable_multidex) {
         proguard_file_main_dex = _generated_proguard_main_dex_config
       }
+      output = _packaged_resources_path
+      density_splits = _density_splits
+      language_splits = _language_splits
 
       build_config = _build_config
       deps = _deps + [
@@ -2134,34 +2141,6 @@
              ]
     }
     _srcjar_deps += [ ":$_process_resources_target" ]
-    _package_resources_target = "${target_name}__package_resources"
-    package_resources(_package_resources_target) {
-      forward_variables_from(invoker,
-                             [
-                               "aapt_locale_whitelist",
-                               "alternative_android_sdk_jar",
-                               "app_as_shared_lib",
-                               "exclude_xxxhdpi",
-                               "png_to_webp",
-                               "shared_resources",
-                               "support_zh_hk",
-                               "xxxhdpi_whitelist",
-                             ])
-      deps = _deps + [
-               ":$_android_manifest_target",
-               ":$_process_resources_target",
-             ]
-      android_manifest = _android_manifest
-      version_code = _version_code
-      version_name = _version_name
-      if (defined(invoker.post_process_package_resources_script)) {
-        post_process_script = invoker.post_process_package_resources_script
-      }
-      resources_zip = _all_resources_zip_path
-      output = _packaged_resources_path
-      density_splits = _density_splits
-      language_splits = _language_splits
-    }
 
     if (_native_libs_deps != []) {
       _enable_chromium_linker_tests = false
@@ -2520,7 +2499,7 @@
       incremental_deps = _deps + [
                            ":$_android_manifest_target",
                            ":$_build_config_target",
-                           ":$_package_resources_target",
+                           ":$_process_resources_target",
                          ]
 
       # This target generates the input file _all_resources_zip_path.
@@ -2528,7 +2507,7 @@
                ":$_android_manifest_target",
                ":$_build_config_target",
                ":$_final_dex_target_name",
-               ":$_package_resources_target",
+               ":$_process_resources_target",
              ]
 
       if ((_native_libs_deps != [] ||
@@ -2570,7 +2549,7 @@
         ]
       }
 
-      package_resources("${_apk_rule}__process_resources") {
+      process_resources("${_apk_rule}__process_resources") {
         deps = [
           ":${_apk_rule}__generate_manifest",
         ]
diff --git a/cc/paint/paint_op_buffer.cc b/cc/paint/paint_op_buffer.cc
index cc496d38..5edbd0b 100644
--- a/cc/paint/paint_op_buffer.cc
+++ b/cc/paint/paint_op_buffer.cc
@@ -347,6 +347,7 @@
     serialized_flags = &op->flags;
   helper.Write(*serialized_flags);
   helper.Write(op->image, options.image_provider);
+  helper.AlignMemory(alignof(SkScalar));
   helper.Write(op->left);
   helper.Write(op->top);
   return helper.size();
@@ -393,6 +394,7 @@
   if (!serialized_flags)
     serialized_flags = &op->flags;
   helper.Write(*serialized_flags);
+  helper.AlignMemory(alignof(SkScalar));
   helper.Write(op->x0);
   helper.Write(op->y0);
   helper.Write(op->x1);
@@ -476,6 +478,7 @@
   if (!serialized_flags)
     serialized_flags = &op->flags;
   helper.Write(*serialized_flags);
+  helper.AlignMemory(alignof(SkScalar));
   helper.Write(op->x);
   helper.Write(op->y);
   helper.Write(op->blob);
@@ -694,6 +697,7 @@
   PaintOpReader helper(input, input_size);
   helper.Read(&op->flags);
   helper.Read(&op->image);
+  helper.AlignMemory(alignof(SkScalar));
   helper.Read(&op->left);
   helper.Read(&op->top);
   if (!helper.valid() || !op->IsValid()) {
@@ -755,6 +759,7 @@
 
   PaintOpReader helper(input, input_size);
   helper.Read(&op->flags);
+  helper.AlignMemory(alignof(SkScalar));
   helper.Read(&op->x0);
   helper.Read(&op->y0);
   helper.Read(&op->x1);
@@ -863,6 +868,7 @@
 
   PaintOpReader helper(input, input_size);
   helper.Read(&op->flags);
+  helper.AlignMemory(alignof(SkScalar));
   helper.Read(&op->x);
   helper.Read(&op->y);
   helper.Read(&op->blob);
@@ -1669,8 +1675,8 @@
   if (written < 4)
     return 0u;
 
-  size_t aligned_written =
-      MathUtil::UncheckedRoundUp(written, PaintOpBuffer::PaintOpAlign);
+  size_t aligned_written = ((written + PaintOpBuffer::PaintOpAlign - 1) &
+                            ~(PaintOpBuffer::PaintOpAlign - 1));
   if (aligned_written >= kMaxSkip)
     return 0u;
   if (aligned_written > size)
diff --git a/cc/paint/paint_op_reader.cc b/cc/paint/paint_op_reader.cc
index b7fa08a..1aa32a7 100644
--- a/cc/paint/paint_op_reader.cc
+++ b/cc/paint/paint_op_reader.cc
@@ -94,8 +94,6 @@
 void PaintOpReader::ReadSimple(T* val) {
   static_assert(base::is_trivially_copyable<T>::value,
                 "Not trivially copyable");
-  if (!AlignMemory(alignof(T)))
-    SetInvalid();
   if (remaining_bytes_ < sizeof(T))
     SetInvalid();
   if (!valid_)
@@ -117,8 +115,6 @@
   ReadSimple(&bytes);
   if (remaining_bytes_ < bytes)
     SetInvalid();
-  if (!SkIsAlign4(reinterpret_cast<uintptr_t>(memory_)))
-    SetInvalid();
   if (!valid_)
     return;
   if (bytes == 0)
@@ -127,6 +123,7 @@
   // This is assumed safe from TOCTOU violations as the flattenable
   // deserializing function uses an SkReadBuffer which reads each piece of
   // memory once much like PaintOpReader does.
+  DCHECK(SkIsAlign4(reinterpret_cast<uintptr_t>(memory_)));
   val->reset(static_cast<T*>(SkValidatingDeserializeFlattenable(
       const_cast<const char*>(memory_), bytes, T::GetFlattenableType())));
   if (!val)
@@ -232,8 +229,11 @@
   // Flattenables must be read at 4-byte boundary, which should be the case
   // here.
   ReadFlattenable(&flags->path_effect_);
+  AlignMemory(4);
   ReadFlattenable(&flags->mask_filter_);
+  AlignMemory(4);
   ReadFlattenable(&flags->color_filter_);
+  AlignMemory(4);
   ReadFlattenable(&flags->draw_looper_);
 
   Read(&flags->shader_);
@@ -490,7 +490,7 @@
   *color_type = static_cast<SkColorType>(raw_color_type);
 }
 
-bool PaintOpReader::AlignMemory(size_t alignment) {
+void PaintOpReader::AlignMemory(size_t alignment) {
   // Due to the math below, alignment must be a power of two.
   DCHECK_GT(alignment, 0u);
   DCHECK_EQ(alignment & (alignment - 1), 0u);
@@ -502,11 +502,10 @@
   // however, since it can be slow.
   size_t padding = ((memory + alignment - 1) & ~(alignment - 1)) - memory;
   if (padding > remaining_bytes_)
-    return false;
+    valid_ = false;
 
   memory_ += padding;
   remaining_bytes_ -= padding;
-  return true;
 }
 
 inline void PaintOpReader::SetInvalid() {
diff --git a/cc/paint/paint_op_reader.h b/cc/paint/paint_op_reader.h
index 075488b5..2594069 100644
--- a/cc/paint/paint_op_reader.h
+++ b/cc/paint/paint_op_reader.h
@@ -82,6 +82,9 @@
   // would exceed the available budfer.
   const volatile void* ExtractReadableMemory(size_t bytes);
 
+  // Aligns the memory to the given alignment.
+  void AlignMemory(size_t alignment);
+
  private:
   template <typename T>
   void ReadSimple(T* val);
@@ -95,10 +98,6 @@
 
   void SetInvalid();
 
-  // Attempts to align the memory to the given alignment. Returns false if there
-  // is unsufficient bytes remaining to do this padding.
-  bool AlignMemory(size_t alignment);
-
   const volatile char* memory_ = nullptr;
   size_t remaining_bytes_ = 0u;
   bool valid_ = true;
diff --git a/cc/paint/paint_op_writer.cc b/cc/paint/paint_op_writer.cc
index bd2f159..f5b1c1a 100644
--- a/cc/paint/paint_op_writer.cc
+++ b/cc/paint/paint_op_writer.cc
@@ -23,8 +23,6 @@
 template <typename T>
 void PaintOpWriter::WriteSimple(const T& val) {
   static_assert(base::is_trivially_copyable<T>::value, "");
-  if (!AlignMemory(alignof(T)))
-    valid_ = false;
   if (remaining_bytes_ < sizeof(T))
     valid_ = false;
   if (!valid_)
@@ -37,13 +35,14 @@
 }
 
 void PaintOpWriter::WriteFlattenable(const SkFlattenable* val) {
+  DCHECK(SkIsAlign4(reinterpret_cast<uintptr_t>(memory_)))
+      << "Flattenable must start writing at 4 byte alignment.";
+
   if (!val) {
     WriteSize(static_cast<size_t>(0u));
     return;
   }
 
-  DCHECK(SkIsAlign4(reinterpret_cast<uintptr_t>(memory_)))
-      << "Flattenable must start writing at 4 byte alignment.";
   // TODO(enne): change skia API to make this a const parameter.
   sk_sp<SkData> data(
       SkValidatingSerializeFlattenable(const_cast<SkFlattenable*>(val)));
@@ -114,8 +113,11 @@
   // Flattenables must be written starting at a 4 byte boundary, which should be
   // the case here.
   WriteFlattenable(flags.path_effect_.get());
+  AlignMemory(4);
   WriteFlattenable(flags.mask_filter_.get());
+  AlignMemory(4);
   WriteFlattenable(flags.color_filter_.get());
+  AlignMemory(4);
   WriteFlattenable(flags.draw_looper_.get());
 
   Write(flags.shader_.get());
@@ -260,7 +262,7 @@
   WriteData(bytes, input);
 }
 
-bool PaintOpWriter::AlignMemory(size_t alignment) {
+void PaintOpWriter::AlignMemory(size_t alignment) {
   // Due to the math below, alignment must be a power of two.
   DCHECK_GT(alignment, 0u);
   DCHECK_EQ(alignment & (alignment - 1), 0u);
@@ -272,11 +274,10 @@
   // however, since it can be slow.
   size_t padding = ((memory + alignment - 1) & ~(alignment - 1)) - memory;
   if (padding > remaining_bytes_)
-    return false;
+    valid_ = false;
 
   memory_ += padding;
   remaining_bytes_ -= padding;
-  return true;
 }
 
 }  // namespace cc
diff --git a/cc/paint/paint_op_writer.h b/cc/paint/paint_op_writer.h
index a2f8a397..ad8eda1 100644
--- a/cc/paint/paint_op_writer.h
+++ b/cc/paint/paint_op_writer.h
@@ -61,6 +61,9 @@
   }
   void Write(bool data) { Write(static_cast<uint8_t>(data)); }
 
+  // Aligns the memory to the given alignment.
+  void AlignMemory(size_t alignment);
+
  private:
   template <typename T>
   void WriteSimple(const T& val);
@@ -71,10 +74,6 @@
 
   static void TypefaceCataloger(SkTypeface* typeface, void* ctx);
 
-  // Attempts to align the memory to the given alignment. Returns false if there
-  // is unsufficient bytes remaining to do this padding.
-  bool AlignMemory(size_t alignment);
-
   char* memory_ = nullptr;
   size_t size_ = 0u;
   size_t remaining_bytes_ = 0u;
diff --git a/cc/scheduler/scheduler.cc b/cc/scheduler/scheduler.cc
index c1c69a9..cd901be4 100644
--- a/cc/scheduler/scheduler.cc
+++ b/cc/scheduler/scheduler.cc
@@ -122,7 +122,7 @@
 }
 
 void Scheduler::SetNeedsPrepareTiles() {
-  DCHECK(!IsInsideAction(SchedulerStateMachine::ACTION_PREPARE_TILES));
+  DCHECK(!IsInsideAction(SchedulerStateMachine::Action::PREPARE_TILES));
   state_machine_.SetNeedsPrepareTiles();
   ProcessScheduledActions();
 }
@@ -134,7 +134,7 @@
   // There is no need to call ProcessScheduledActions here because
   // submitting a CompositorFrame should not trigger any new actions.
   if (!inside_process_scheduled_actions_) {
-    DCHECK_EQ(state_machine_.NextAction(), SchedulerStateMachine::ACTION_NONE);
+    DCHECK_EQ(state_machine_.NextAction(), SchedulerStateMachine::Action::NONE);
   }
 }
 
@@ -228,7 +228,7 @@
 
 void Scheduler::SetupNextBeginFrameIfNeeded() {
   if (state_machine_.begin_impl_frame_state() !=
-      SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_IDLE) {
+      SchedulerStateMachine::BeginImplFrameState::IDLE) {
     return;
   }
 
@@ -319,7 +319,7 @@
 void Scheduler::OnDrawForLayerTreeFrameSink(bool resourceless_software_draw) {
   DCHECK(settings_.using_synchronous_renderer_compositor);
   DCHECK_EQ(state_machine_.begin_impl_frame_state(),
-            SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_IDLE);
+            SchedulerStateMachine::BeginImplFrameState::IDLE);
   DCHECK(begin_impl_frame_deadline_task_.IsCancelled());
 
   state_machine_.SetResourcelessSoftwareDraw(resourceless_software_draw);
@@ -355,7 +355,7 @@
 
   // Run the previous deadline if any.
   if (state_machine_.begin_impl_frame_state() ==
-      SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_BEGIN_FRAME) {
+      SchedulerStateMachine::BeginImplFrameState::INSIDE_BEGIN_FRAME) {
     OnBeginImplFrameDeadline();
     // We may not need begin frames any longer.
     if (!observing_begin_frame_source_) {
@@ -365,7 +365,7 @@
     }
   }
   DCHECK_EQ(state_machine_.begin_impl_frame_state(),
-            SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_IDLE);
+            SchedulerStateMachine::BeginImplFrameState::IDLE);
 
   bool main_thread_is_in_high_latency_mode =
       state_machine_.main_thread_missed_last_deadline();
@@ -376,7 +376,7 @@
                  "MainThreadLatency", main_thread_is_in_high_latency_mode);
 
   DCHECK_EQ(state_machine_.begin_impl_frame_state(),
-            SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_IDLE);
+            SchedulerStateMachine::BeginImplFrameState::IDLE);
 
   adjusted_args.deadline -= compositor_timing_history_->DrawDurationEstimate();
   adjusted_args.deadline -= kDeadlineFudgeFactor;
@@ -392,10 +392,10 @@
       compositor_timing_history_->BeginMainFrameQueueDurationCriticalEstimate();
 
   // TODO(khushalsagar): We need to consider the deadline fudge factor here to
-  // match the deadline used in BEGIN_IMPL_FRAME_DEADLINE_MODE_REGULAR mode
+  // match the deadline used in BeginImplFrameDeadlineMode::REGULAR mode
   // (used in the case where the impl thread needs to redraw). In the case where
   // main_frame_to_active is fast, we should consider using
-  // BEGIN_IMPL_FRAME_DEADLINE_MODE_LATE instead to avoid putting the main
+  // BeginImplFrameDeadlineMode::LATE instead to avoid putting the main
   // thread in high latency mode. See crbug.com/753146.
   base::TimeDelta bmf_to_activate_threshold =
       adjusted_args.interval -
@@ -484,7 +484,7 @@
 void Scheduler::BeginImplFrame(const viz::BeginFrameArgs& args,
                                base::TimeTicks now) {
   DCHECK_EQ(state_machine_.begin_impl_frame_state(),
-            SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_IDLE);
+            SchedulerStateMachine::BeginImplFrameState::IDLE);
   DCHECK(begin_impl_frame_deadline_task_.IsCancelled());
   DCHECK(state_machine_.HasInitializedLayerTreeFrameSink());
 
@@ -508,26 +508,26 @@
   begin_impl_frame_deadline_mode_ =
       state_machine_.CurrentBeginImplFrameDeadlineMode();
   switch (begin_impl_frame_deadline_mode_) {
-    case SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_NONE:
+    case SchedulerStateMachine::BeginImplFrameDeadlineMode::NONE:
       // No deadline.
       return;
-    case SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_IMMEDIATE:
+    case SchedulerStateMachine::BeginImplFrameDeadlineMode::IMMEDIATE:
       // We are ready to draw a new active tree immediately.
       // We don't use Now() here because it's somewhat expensive to call.
       deadline_ = base::TimeTicks();
       break;
-    case SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_REGULAR:
+    case SchedulerStateMachine::BeginImplFrameDeadlineMode::REGULAR:
       // We are animating on the impl thread but we can wait for some time.
       deadline_ = begin_impl_frame_tracker_.Current().deadline;
       break;
-    case SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_LATE:
+    case SchedulerStateMachine::BeginImplFrameDeadlineMode::LATE:
       // We are blocked for one reason or another and we should wait.
       // TODO(brianderson): Handle long deadlines (that are past the next
       // frame's frame time) properly instead of using this hack.
       deadline_ = begin_impl_frame_tracker_.Current().frame_time +
                   begin_impl_frame_tracker_.Current().interval;
       break;
-    case SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_BLOCKED:
+    case SchedulerStateMachine::BeginImplFrameDeadlineMode::BLOCKED:
       // We are blocked because we are waiting for ReadyToDraw signal. We would
       // post deadline after we received ReadyToDraw singal.
       TRACE_EVENT1("cc", "Scheduler::ScheduleBeginImplFrameDeadline",
@@ -552,7 +552,7 @@
     return;
 
   if (state_machine_.begin_impl_frame_state() !=
-      SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_BEGIN_FRAME)
+      SchedulerStateMachine::BeginImplFrameState::INSIDE_BEGIN_FRAME)
     return;
 
   if (begin_impl_frame_deadline_mode_ ==
@@ -645,9 +645,9 @@
     base::AutoReset<SchedulerStateMachine::Action> mark_inside_action(
         &inside_action_, action);
     switch (action) {
-      case SchedulerStateMachine::ACTION_NONE:
+      case SchedulerStateMachine::Action::NONE:
         break;
-      case SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME:
+      case SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME:
         compositor_timing_history_->WillBeginMainFrame(
             begin_main_frame_args_.on_critical_path,
             begin_main_frame_args_.frame_time);
@@ -655,7 +655,7 @@
         // TODO(brianderson): Pass begin_main_frame_args_ directly to client.
         client_->ScheduledActionSendBeginMainFrame(begin_main_frame_args_);
         break;
-      case SchedulerStateMachine::ACTION_NOTIFY_BEGIN_MAIN_FRAME_NOT_SENT:
+      case SchedulerStateMachine::Action::NOTIFY_BEGIN_MAIN_FRAME_NOT_SENT:
         state_machine_.WillNotifyBeginMainFrameNotSent();
         // If SendBeginMainFrameNotExpectedSoon was not previously sent by
         // BeginImplFrameNotExpectedSoon (because the messages were not required
@@ -667,51 +667,51 @@
                                          begin_main_frame_args_.interval);
         }
         break;
-      case SchedulerStateMachine::ACTION_COMMIT: {
+      case SchedulerStateMachine::Action::COMMIT: {
         bool commit_has_no_updates = false;
         state_machine_.WillCommit(commit_has_no_updates);
         client_->ScheduledActionCommit();
         break;
       }
-      case SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE:
+      case SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE:
         compositor_timing_history_->WillActivate();
         state_machine_.WillActivate();
         client_->ScheduledActionActivateSyncTree();
         compositor_timing_history_->DidActivate();
         break;
-      case SchedulerStateMachine::ACTION_PERFORM_IMPL_SIDE_INVALIDATION:
+      case SchedulerStateMachine::Action::PERFORM_IMPL_SIDE_INVALIDATION:
         state_machine_.WillPerformImplSideInvalidation();
         compositor_timing_history_->WillInvalidateOnImplSide();
         client_->ScheduledActionPerformImplSideInvalidation();
         break;
-      case SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE:
+      case SchedulerStateMachine::Action::DRAW_IF_POSSIBLE:
         DrawIfPossible();
         break;
-      case SchedulerStateMachine::ACTION_DRAW_FORCED:
+      case SchedulerStateMachine::Action::DRAW_FORCED:
         DrawForced();
         break;
-      case SchedulerStateMachine::ACTION_DRAW_ABORT: {
+      case SchedulerStateMachine::Action::DRAW_ABORT: {
         // No action is actually performed, but this allows the state machine to
         // drain the pipeline without actually drawing.
         state_machine_.AbortDraw();
         compositor_timing_history_->DrawAborted();
         break;
       }
-      case SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION:
+      case SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION:
         state_machine_.WillBeginLayerTreeFrameSinkCreation();
         client_->ScheduledActionBeginLayerTreeFrameSinkCreation();
         break;
-      case SchedulerStateMachine::ACTION_PREPARE_TILES:
+      case SchedulerStateMachine::Action::PREPARE_TILES:
         state_machine_.WillPrepareTiles();
         client_->ScheduledActionPrepareTiles();
         break;
-      case SchedulerStateMachine::ACTION_INVALIDATE_LAYER_TREE_FRAME_SINK: {
+      case SchedulerStateMachine::Action::INVALIDATE_LAYER_TREE_FRAME_SINK: {
         state_machine_.WillInvalidateLayerTreeFrameSink();
         client_->ScheduledActionInvalidateLayerTreeFrameSink();
         break;
       }
     }
-  } while (action != SchedulerStateMachine::ACTION_NONE);
+  } while (action != SchedulerStateMachine::Action::NONE);
 
   ScheduleBeginImplFrameDeadlineIfNeeded();
   SetupNextBeginFrameIfNeeded();
@@ -876,9 +876,9 @@
 
 bool Scheduler::IsBeginMainFrameSentOrStarted() const {
   return (state_machine_.begin_main_frame_state() ==
-              SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT ||
+              SchedulerStateMachine::BeginMainFrameState::SENT ||
           state_machine_.begin_main_frame_state() ==
-              SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_STARTED);
+              SchedulerStateMachine::BeginMainFrameState::STARTED);
 }
 
 viz::BeginFrameAck Scheduler::CurrentBeginFrameAckForActiveTree() const {
diff --git a/cc/scheduler/scheduler.h b/cc/scheduler/scheduler.h
index 94e1c62..5e2dcb9 100644
--- a/cc/scheduler/scheduler.h
+++ b/cc/scheduler/scheduler.h
@@ -189,7 +189,7 @@
 
   SchedulerStateMachine::BeginImplFrameDeadlineMode
       begin_impl_frame_deadline_mode_ =
-          SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_NONE;
+          SchedulerStateMachine::BeginImplFrameDeadlineMode::NONE;
   base::TimeTicks deadline_;
   base::TimeTicks deadline_scheduled_at_;
 
@@ -203,7 +203,7 @@
   SchedulerStateMachine state_machine_;
   bool inside_process_scheduled_actions_ = false;
   SchedulerStateMachine::Action inside_action_ =
-      SchedulerStateMachine::ACTION_NONE;
+      SchedulerStateMachine::Action::NONE;
 
   bool stopped_ = false;
 
diff --git a/cc/scheduler/scheduler_state_machine.cc b/cc/scheduler/scheduler_state_machine.cc
index 476f1ae1..f8279d3c 100644
--- a/cc/scheduler/scheduler_state_machine.cc
+++ b/cc/scheduler/scheduler_state_machine.cc
@@ -25,16 +25,16 @@
 const char* SchedulerStateMachine::LayerTreeFrameSinkStateToString(
     LayerTreeFrameSinkState state) {
   switch (state) {
-    case LAYER_TREE_FRAME_SINK_NONE:
-      return "LAYER_TREE_FRAME_SINK_NONE";
-    case LAYER_TREE_FRAME_SINK_ACTIVE:
-      return "LAYER_TREE_FRAME_SINK_ACTIVE";
-    case LAYER_TREE_FRAME_SINK_CREATING:
-      return "LAYER_TREE_FRAME_SINK_CREATING";
-    case LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_COMMIT:
-      return "LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_COMMIT";
-    case LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_ACTIVATION:
-      return "LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_ACTIVATION";
+    case LayerTreeFrameSinkState::NONE:
+      return "LayerTreeFrameSinkState::NONE";
+    case LayerTreeFrameSinkState::ACTIVE:
+      return "LayerTreeFrameSinkState::ACTIVE";
+    case LayerTreeFrameSinkState::CREATING:
+      return "LayerTreeFrameSinkState::CREATING";
+    case LayerTreeFrameSinkState::WAITING_FOR_FIRST_COMMIT:
+      return "LayerTreeFrameSinkState::WAITING_FOR_FIRST_COMMIT";
+    case LayerTreeFrameSinkState::WAITING_FOR_FIRST_ACTIVATION:
+      return "LayerTreeFrameSinkState::WAITING_FOR_FIRST_ACTIVATION";
   }
   NOTREACHED();
   return "???";
@@ -43,12 +43,12 @@
 const char* SchedulerStateMachine::BeginImplFrameStateToString(
     BeginImplFrameState state) {
   switch (state) {
-    case BEGIN_IMPL_FRAME_STATE_IDLE:
-      return "BEGIN_IMPL_FRAME_STATE_IDLE";
-    case BEGIN_IMPL_FRAME_STATE_INSIDE_BEGIN_FRAME:
-      return "BEGIN_IMPL_FRAME_STATE_INSIDE_BEGIN_FRAME";
-    case BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE:
-      return "BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE";
+    case BeginImplFrameState::IDLE:
+      return "BeginImplFrameState::IDLE";
+    case BeginImplFrameState::INSIDE_BEGIN_FRAME:
+      return "BeginImplFrameState::INSIDE_BEGIN_FRAME";
+    case BeginImplFrameState::INSIDE_DEADLINE:
+      return "BeginImplFrameState::INSIDE_DEADLINE";
   }
   NOTREACHED();
   return "???";
@@ -57,16 +57,16 @@
 const char* SchedulerStateMachine::BeginImplFrameDeadlineModeToString(
     BeginImplFrameDeadlineMode mode) {
   switch (mode) {
-    case BEGIN_IMPL_FRAME_DEADLINE_MODE_NONE:
-      return "BEGIN_IMPL_FRAME_DEADLINE_MODE_NONE";
-    case BEGIN_IMPL_FRAME_DEADLINE_MODE_IMMEDIATE:
-      return "BEGIN_IMPL_FRAME_DEADLINE_MODE_IMMEDIATE";
-    case BEGIN_IMPL_FRAME_DEADLINE_MODE_REGULAR:
-      return "BEGIN_IMPL_FRAME_DEADLINE_MODE_REGULAR";
-    case BEGIN_IMPL_FRAME_DEADLINE_MODE_LATE:
-      return "BEGIN_IMPL_FRAME_DEADLINE_MODE_LATE";
-    case BEGIN_IMPL_FRAME_DEADLINE_MODE_BLOCKED:
-      return "BEGIN_IMPL_FRAME_DEADLINE_MODE_BLOCKED";
+    case BeginImplFrameDeadlineMode::NONE:
+      return "BeginImplFrameDeadlineMode::NONE";
+    case BeginImplFrameDeadlineMode::IMMEDIATE:
+      return "BeginImplFrameDeadlineMode::IMMEDIATE";
+    case BeginImplFrameDeadlineMode::REGULAR:
+      return "BeginImplFrameDeadlineMode::REGULAR";
+    case BeginImplFrameDeadlineMode::LATE:
+      return "BeginImplFrameDeadlineMode::LATE";
+    case BeginImplFrameDeadlineMode::BLOCKED:
+      return "BeginImplFrameDeadlineMode::BLOCKED";
   }
   NOTREACHED();
   return "???";
@@ -75,14 +75,14 @@
 const char* SchedulerStateMachine::BeginMainFrameStateToString(
     BeginMainFrameState state) {
   switch (state) {
-    case BEGIN_MAIN_FRAME_STATE_IDLE:
-      return "BEGIN_MAIN_FRAME_STATE_IDLE";
-    case BEGIN_MAIN_FRAME_STATE_SENT:
-      return "BEGIN_MAIN_FRAME_STATE_SENT";
-    case BEGIN_MAIN_FRAME_STATE_STARTED:
-      return "BEGIN_MAIN_FRAME_STATE_STARTED";
-    case BEGIN_MAIN_FRAME_STATE_READY_TO_COMMIT:
-      return "BEGIN_MAIN_FRAME_STATE_READY_TO_COMMIT";
+    case BeginMainFrameState::IDLE:
+      return "BeginMainFrameState::IDLE";
+    case BeginMainFrameState::SENT:
+      return "BeginMainFrameState::SENT";
+    case BeginMainFrameState::STARTED:
+      return "BeginMainFrameState::STARTED";
+    case BeginMainFrameState::READY_TO_COMMIT:
+      return "BeginMainFrameState::READY_TO_COMMIT";
   }
   NOTREACHED();
   return "???";
@@ -91,14 +91,14 @@
 const char* SchedulerStateMachine::ForcedRedrawOnTimeoutStateToString(
     ForcedRedrawOnTimeoutState state) {
   switch (state) {
-    case FORCED_REDRAW_STATE_IDLE:
-      return "FORCED_REDRAW_STATE_IDLE";
-    case FORCED_REDRAW_STATE_WAITING_FOR_COMMIT:
-      return "FORCED_REDRAW_STATE_WAITING_FOR_COMMIT";
-    case FORCED_REDRAW_STATE_WAITING_FOR_ACTIVATION:
-      return "FORCED_REDRAW_STATE_WAITING_FOR_ACTIVATION";
-    case FORCED_REDRAW_STATE_WAITING_FOR_DRAW:
-      return "FORCED_REDRAW_STATE_WAITING_FOR_DRAW";
+    case ForcedRedrawOnTimeoutState::IDLE:
+      return "ForcedRedrawOnTimeoutState::IDLE";
+    case ForcedRedrawOnTimeoutState::WAITING_FOR_COMMIT:
+      return "ForcedRedrawOnTimeoutState::WAITING_FOR_COMMIT";
+    case ForcedRedrawOnTimeoutState::WAITING_FOR_ACTIVATION:
+      return "ForcedRedrawOnTimeoutState::WAITING_FOR_ACTIVATION";
+    case ForcedRedrawOnTimeoutState::WAITING_FOR_DRAW:
+      return "ForcedRedrawOnTimeoutState::WAITING_FOR_DRAW";
   }
   NOTREACHED();
   return "???";
@@ -117,30 +117,30 @@
 
 const char* SchedulerStateMachine::ActionToString(Action action) {
   switch (action) {
-    case ACTION_NONE:
-      return "ACTION_NONE";
-    case ACTION_SEND_BEGIN_MAIN_FRAME:
-      return "ACTION_SEND_BEGIN_MAIN_FRAME";
-    case ACTION_COMMIT:
-      return "ACTION_COMMIT";
-    case ACTION_ACTIVATE_SYNC_TREE:
-      return "ACTION_ACTIVATE_SYNC_TREE";
-    case ACTION_DRAW_IF_POSSIBLE:
-      return "ACTION_DRAW_IF_POSSIBLE";
-    case ACTION_DRAW_FORCED:
-      return "ACTION_DRAW_FORCED";
-    case ACTION_DRAW_ABORT:
-      return "ACTION_DRAW_ABORT";
-    case ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION:
-      return "ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION";
-    case ACTION_PREPARE_TILES:
-      return "ACTION_PREPARE_TILES";
-    case ACTION_INVALIDATE_LAYER_TREE_FRAME_SINK:
-      return "ACTION_INVALIDATE_LAYER_TREE_FRAME_SINK";
-    case ACTION_PERFORM_IMPL_SIDE_INVALIDATION:
-      return "ACTION_PERFORM_IMPL_SIDE_INVALIDATION";
-    case ACTION_NOTIFY_BEGIN_MAIN_FRAME_NOT_SENT:
-      return "ACTION_NOTIFY_BEGIN_MAIN_FRAME_NOT_SENT";
+    case Action::NONE:
+      return "Action::NONE";
+    case Action::SEND_BEGIN_MAIN_FRAME:
+      return "Action::SEND_BEGIN_MAIN_FRAME";
+    case Action::COMMIT:
+      return "Action::COMMIT";
+    case Action::ACTIVATE_SYNC_TREE:
+      return "Action::ACTIVATE_SYNC_TREE";
+    case Action::DRAW_IF_POSSIBLE:
+      return "Action::DRAW_IF_POSSIBLE";
+    case Action::DRAW_FORCED:
+      return "Action::DRAW_FORCED";
+    case Action::DRAW_ABORT:
+      return "Action::DRAW_ABORT";
+    case Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION:
+      return "Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION";
+    case Action::PREPARE_TILES:
+      return "Action::PREPARE_TILES";
+    case Action::INVALIDATE_LAYER_TREE_FRAME_SINK:
+      return "Action::INVALIDATE_LAYER_TREE_FRAME_SINK";
+    case Action::PERFORM_IMPL_SIDE_INVALIDATION:
+      return "Action::PERFORM_IMPL_SIDE_INVALIDATION";
+    case Action::NOTIFY_BEGIN_MAIN_FRAME_NOT_SENT:
+      return "Action::NOTIFY_BEGIN_MAIN_FRAME_NOT_SENT";
   }
   NOTREACHED();
   return "???";
@@ -242,7 +242,7 @@
   // when the embedder is Android WebView, software draws could be scheduled by
   // the Android OS at any time and draws should not be aborted in this case.
   bool is_layer_tree_frame_sink_lost =
-      (layer_tree_frame_sink_state_ == LAYER_TREE_FRAME_SINK_NONE);
+      (layer_tree_frame_sink_state_ == LayerTreeFrameSinkState::NONE);
   if (resourceless_draw_)
     return is_layer_tree_frame_sink_lost || !can_draw_;
 
@@ -260,7 +260,7 @@
 bool SchedulerStateMachine::ShouldAbortCurrentFrame() const {
   // Abort the frame if there is no output surface to trigger our
   // activations, avoiding deadlock with the main thread.
-  if (layer_tree_frame_sink_state_ == LAYER_TREE_FRAME_SINK_NONE)
+  if (layer_tree_frame_sink_state_ == LayerTreeFrameSinkState::NONE)
     return true;
 
   // If we're not visible, we should just abort the frame. Since we
@@ -288,13 +288,13 @@
 
   // We only want to start output surface initialization after the
   // previous commit is complete.
-  if (begin_main_frame_state_ != BEGIN_MAIN_FRAME_STATE_IDLE) {
+  if (begin_main_frame_state_ != BeginMainFrameState::IDLE) {
     return false;
   }
 
   // Make sure the BeginImplFrame from any previous LayerTreeFrameSinks
   // are complete before creating the new LayerTreeFrameSink.
-  if (begin_impl_frame_state_ != BEGIN_IMPL_FRAME_STATE_IDLE)
+  if (begin_impl_frame_state_ != BeginImplFrameState::IDLE)
     return false;
 
   // We want to clear the pipeline of any pending draws and activations
@@ -306,7 +306,7 @@
 
   // We need to create the output surface if we don't have one and we haven't
   // started creating one yet.
-  return layer_tree_frame_sink_state_ == LAYER_TREE_FRAME_SINK_NONE;
+  return layer_tree_frame_sink_state_ == LayerTreeFrameSinkState::NONE;
 }
 
 bool SchedulerStateMachine::ShouldDraw() const {
@@ -324,7 +324,7 @@
     return false;
 
   // Don't draw if we are waiting on the first commit after a surface.
-  if (layer_tree_frame_sink_state_ != LAYER_TREE_FRAME_SINK_ACTIVE)
+  if (layer_tree_frame_sink_state_ != LayerTreeFrameSinkState::ACTIVE)
     return false;
 
   // Do not queue too many draws.
@@ -333,7 +333,7 @@
 
   // Except for the cases above, do not draw outside of the BeginImplFrame
   // deadline.
-  if (begin_impl_frame_state_ != BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE)
+  if (begin_impl_frame_state_ != BeginImplFrameState::INSIDE_DEADLINE)
     return false;
 
   // Wait for ready to draw in full-pipeline mode or the browser compositor's
@@ -350,7 +350,7 @@
     return false;
 
   // Only handle forced redraws due to timeouts on the regular deadline.
-  if (forced_redraw_state_ == FORCED_REDRAW_STATE_WAITING_FOR_DRAW)
+  if (forced_redraw_state_ == ForcedRedrawOnTimeoutState::WAITING_FOR_DRAW)
     return true;
 
   return needs_redraw_;
@@ -385,7 +385,7 @@
   // Don't notify if a BeginMainFrame has already been requested or is in
   // progress.
   if (needs_begin_main_frame_ ||
-      begin_main_frame_state_ != BEGIN_MAIN_FRAME_STATE_IDLE)
+      begin_main_frame_state_ != BeginMainFrameState::IDLE)
     return false;
 
   // Only notify when we're visible.
@@ -445,7 +445,7 @@
   // Other parts of the state machine indirectly defer the BeginMainFrame
   // by transitioning to WAITING commit states rather than going
   // immediately to IDLE.
-  if (begin_main_frame_state_ != BEGIN_MAIN_FRAME_STATE_IDLE)
+  if (begin_main_frame_state_ != BeginMainFrameState::IDLE)
     return false;
 
   // MFBA is disabled and we are waiting for previous activation.
@@ -472,13 +472,13 @@
   // TODO(brianderson): Allow sending BeginMainFrame while idle when the main
   // thread isn't consuming user input for non-synchronous compositor.
   if (!settings_.using_synchronous_renderer_compositor &&
-      begin_impl_frame_state_ == BEGIN_IMPL_FRAME_STATE_IDLE) {
+      begin_impl_frame_state_ == BeginImplFrameState::IDLE) {
     return false;
   }
 
   // We need a new commit for the forced redraw. This honors the
   // single commit per interval because the result will be swapped to screen.
-  if (forced_redraw_state_ == FORCED_REDRAW_STATE_WAITING_FOR_COMMIT)
+  if (forced_redraw_state_ == ForcedRedrawOnTimeoutState::WAITING_FOR_COMMIT)
     return true;
 
   // We shouldn't normally accept commits if there isn't a LayerTreeFrameSink.
@@ -492,7 +492,7 @@
     // TODO(brianderson): Remove this restriction to improve throughput or
     // make it conditional on ImplLatencyTakesPriority.
     bool just_submitted_in_deadline =
-        begin_impl_frame_state_ == BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE &&
+        begin_impl_frame_state_ == BeginImplFrameState::INSIDE_DEADLINE &&
         did_submit_in_last_frame_;
     if (IsDrawThrottled() && !just_submitted_in_deadline)
       return false;
@@ -505,7 +505,7 @@
 }
 
 bool SchedulerStateMachine::ShouldCommit() const {
-  if (begin_main_frame_state_ != BEGIN_MAIN_FRAME_STATE_READY_TO_COMMIT)
+  if (begin_main_frame_state_ != BeginMainFrameState::READY_TO_COMMIT)
     return false;
 
   // We must not finish the commit until the pending tree is free.
@@ -538,7 +538,7 @@
 
   // Limiting to once per-frame is not enough, since we only want to prepare
   // tiles _after_ draws.
-  if (begin_impl_frame_state_ != BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE)
+  if (begin_impl_frame_state_ != BeginImplFrameState::INSIDE_DEADLINE)
     return false;
 
   return needs_prepare_tiles_;
@@ -554,7 +554,7 @@
     return false;
 
   // Invalidations are only performed inside a BeginFrame.
-  if (begin_impl_frame_state_ != BEGIN_IMPL_FRAME_STATE_INSIDE_BEGIN_FRAME)
+  if (begin_impl_frame_state_ != BeginImplFrameState::INSIDE_BEGIN_FRAME)
     return false;
 
   // TODO(sunnyps): needs_prepare_tiles_ is needed here because PrepareTiles is
@@ -565,30 +565,31 @@
 
 SchedulerStateMachine::Action SchedulerStateMachine::NextAction() const {
   if (ShouldActivateSyncTree())
-    return ACTION_ACTIVATE_SYNC_TREE;
+    return Action::ACTIVATE_SYNC_TREE;
   if (ShouldCommit())
-    return ACTION_COMMIT;
+    return Action::COMMIT;
   if (ShouldDraw()) {
     if (PendingDrawsShouldBeAborted())
-      return ACTION_DRAW_ABORT;
-    else if (forced_redraw_state_ == FORCED_REDRAW_STATE_WAITING_FOR_DRAW)
-      return ACTION_DRAW_FORCED;
+      return Action::DRAW_ABORT;
+    else if (forced_redraw_state_ ==
+             ForcedRedrawOnTimeoutState::WAITING_FOR_DRAW)
+      return Action::DRAW_FORCED;
     else
-      return ACTION_DRAW_IF_POSSIBLE;
+      return Action::DRAW_IF_POSSIBLE;
   }
   if (ShouldPerformImplSideInvalidation())
-    return ACTION_PERFORM_IMPL_SIDE_INVALIDATION;
+    return Action::PERFORM_IMPL_SIDE_INVALIDATION;
   if (ShouldPrepareTiles())
-    return ACTION_PREPARE_TILES;
+    return Action::PREPARE_TILES;
   if (ShouldSendBeginMainFrame())
-    return ACTION_SEND_BEGIN_MAIN_FRAME;
+    return Action::SEND_BEGIN_MAIN_FRAME;
   if (ShouldInvalidateLayerTreeFrameSink())
-    return ACTION_INVALIDATE_LAYER_TREE_FRAME_SINK;
+    return Action::INVALIDATE_LAYER_TREE_FRAME_SINK;
   if (ShouldBeginLayerTreeFrameSinkCreation())
-    return ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION;
+    return Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION;
   if (ShouldNotifyBeginMainFrameNotSent())
-    return ACTION_NOTIFY_BEGIN_MAIN_FRAME_NOT_SENT;
-  return ACTION_NONE;
+    return Action::NOTIFY_BEGIN_MAIN_FRAME_NOT_SENT;
+  return Action::NONE;
 }
 
 bool SchedulerStateMachine::ShouldPerformImplSideInvalidation() const {
@@ -601,7 +602,7 @@
     return false;
 
   // No invalidations should be done outside the impl frame.
-  if (begin_impl_frame_state_ == BEGIN_IMPL_FRAME_STATE_IDLE)
+  if (begin_impl_frame_state_ == BeginImplFrameState::IDLE)
     return false;
 
   // We need to be able to create a pending tree to perform an invalidation.
@@ -624,11 +625,11 @@
 }
 
 bool SchedulerStateMachine::ShouldDeferInvalidatingForMainFrame() const {
-  DCHECK_NE(begin_impl_frame_state_, BEGIN_IMPL_FRAME_STATE_IDLE);
+  DCHECK_NE(begin_impl_frame_state_, BeginImplFrameState::IDLE);
 
   // If the main thread is ready to commit, the impl-side invalidations will be
   // merged with the incoming main frame.
-  if (begin_main_frame_state_ == BEGIN_MAIN_FRAME_STATE_READY_TO_COMMIT)
+  if (begin_main_frame_state_ == BeginMainFrameState::READY_TO_COMMIT)
     return true;
 
   // If we are inside the deadline, and haven't performed an invalidation yet,
@@ -641,7 +642,7 @@
   // b) We have to wait on the main thread to respond to a main frame.
   // In addition, the deadline task can be cancelled if the main thread
   // responds before it runs.
-  if (begin_impl_frame_state_ == BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE)
+  if (begin_impl_frame_state_ == BeginImplFrameState::INSIDE_DEADLINE)
     return false;
 
   // If commits are being aborted (which would be the common case for a
@@ -658,8 +659,8 @@
     return true;
 
   // If the main frame was already sent, wait for the main thread to respond.
-  if (begin_main_frame_state_ == BEGIN_MAIN_FRAME_STATE_SENT ||
-      begin_main_frame_state_ == BEGIN_MAIN_FRAME_STATE_STARTED)
+  if (begin_main_frame_state_ == BeginMainFrameState::SENT ||
+      begin_main_frame_state_ == BeginMainFrameState::STARTED)
     return true;
 
   // If the main thread committed during the last frame, i.e. it was not
@@ -720,7 +721,7 @@
   DCHECK(visible_);
   DCHECK(!begin_frame_source_paused_);
   DCHECK(!did_send_begin_main_frame_for_current_frame_);
-  begin_main_frame_state_ = BEGIN_MAIN_FRAME_STATE_SENT;
+  begin_main_frame_state_ = BeginMainFrameState::SENT;
   needs_begin_main_frame_ = false;
   did_send_begin_main_frame_for_current_frame_ = true;
   last_frame_number_begin_main_frame_sent_ = current_frame_number_;
@@ -741,7 +742,7 @@
   DCHECK(!has_pending_tree_ || can_have_pending_tree);
   commit_count_++;
   last_commit_had_no_updates_ = commit_has_no_updates;
-  begin_main_frame_state_ = BEGIN_MAIN_FRAME_STATE_IDLE;
+  begin_main_frame_state_ = BeginMainFrameState::IDLE;
   did_commit_during_frame_ = true;
 
   if (!commit_has_no_updates) {
@@ -764,28 +765,30 @@
   }
 
   // Update state related to forced draws.
-  if (forced_redraw_state_ == FORCED_REDRAW_STATE_WAITING_FOR_COMMIT) {
-    forced_redraw_state_ = has_pending_tree_
-                               ? FORCED_REDRAW_STATE_WAITING_FOR_ACTIVATION
-                               : FORCED_REDRAW_STATE_WAITING_FOR_DRAW;
+  if (forced_redraw_state_ == ForcedRedrawOnTimeoutState::WAITING_FOR_COMMIT) {
+    forced_redraw_state_ =
+        has_pending_tree_ ? ForcedRedrawOnTimeoutState::WAITING_FOR_ACTIVATION
+                          : ForcedRedrawOnTimeoutState::WAITING_FOR_DRAW;
   }
 
   // Update the output surface state.
   if (layer_tree_frame_sink_state_ ==
-      LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_COMMIT) {
+      LayerTreeFrameSinkState::WAITING_FOR_FIRST_COMMIT) {
     layer_tree_frame_sink_state_ =
-        has_pending_tree_ ? LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_ACTIVATION
-                          : LAYER_TREE_FRAME_SINK_ACTIVE;
+        has_pending_tree_
+            ? LayerTreeFrameSinkState::WAITING_FOR_FIRST_ACTIVATION
+            : LayerTreeFrameSinkState::ACTIVE;
   }
 }
 
 void SchedulerStateMachine::WillActivate() {
   if (layer_tree_frame_sink_state_ ==
-      LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_ACTIVATION)
-    layer_tree_frame_sink_state_ = LAYER_TREE_FRAME_SINK_ACTIVE;
+      LayerTreeFrameSinkState::WAITING_FOR_FIRST_ACTIVATION)
+    layer_tree_frame_sink_state_ = LayerTreeFrameSinkState::ACTIVE;
 
-  if (forced_redraw_state_ == FORCED_REDRAW_STATE_WAITING_FOR_ACTIVATION)
-    forced_redraw_state_ = FORCED_REDRAW_STATE_WAITING_FOR_DRAW;
+  if (forced_redraw_state_ ==
+      ForcedRedrawOnTimeoutState::WAITING_FOR_ACTIVATION)
+    forced_redraw_state_ = ForcedRedrawOnTimeoutState::WAITING_FOR_DRAW;
 
   has_pending_tree_ = false;
   pending_tree_is_ready_for_activation_ = false;
@@ -814,8 +817,8 @@
   did_draw_in_last_frame_ = true;
   last_frame_number_draw_performed_ = current_frame_number_;
 
-  if (forced_redraw_state_ == FORCED_REDRAW_STATE_WAITING_FOR_DRAW)
-    forced_redraw_state_ = FORCED_REDRAW_STATE_IDLE;
+  if (forced_redraw_state_ == ForcedRedrawOnTimeoutState::WAITING_FOR_DRAW)
+    forced_redraw_state_ = ForcedRedrawOnTimeoutState::IDLE;
 }
 
 void SchedulerStateMachine::DidDrawInternal(DrawResult draw_result) {
@@ -827,7 +830,7 @@
     case DRAW_ABORTED_DRAINING_PIPELINE:
     case DRAW_SUCCESS:
       consecutive_checkerboard_animations_ = 0;
-      forced_redraw_state_ = FORCED_REDRAW_STATE_IDLE;
+      forced_redraw_state_ = ForcedRedrawOnTimeoutState::IDLE;
       break;
     case DRAW_ABORTED_CHECKERBOARD_ANIMATIONS:
       DCHECK(!did_submit_in_last_frame_);
@@ -837,11 +840,11 @@
 
       if (consecutive_checkerboard_animations_ >=
               settings_.maximum_number_of_failed_draws_before_draw_is_forced &&
-          forced_redraw_state_ == FORCED_REDRAW_STATE_IDLE &&
+          forced_redraw_state_ == ForcedRedrawOnTimeoutState::IDLE &&
           settings_.timeout_and_draw_when_animation_checkerboards) {
         // We need to force a draw, but it doesn't make sense to do this until
         // we've committed and have new textures.
-        forced_redraw_state_ = FORCED_REDRAW_STATE_WAITING_FOR_COMMIT;
+        forced_redraw_state_ = ForcedRedrawOnTimeoutState::WAITING_FOR_COMMIT;
       }
       break;
     case DRAW_ABORTED_MISSING_HIGH_RES_CONTENT:
@@ -889,13 +892,13 @@
 }
 
 void SchedulerStateMachine::WillBeginLayerTreeFrameSinkCreation() {
-  DCHECK_EQ(layer_tree_frame_sink_state_, LAYER_TREE_FRAME_SINK_NONE);
-  layer_tree_frame_sink_state_ = LAYER_TREE_FRAME_SINK_CREATING;
+  DCHECK_EQ(layer_tree_frame_sink_state_, LayerTreeFrameSinkState::NONE);
+  layer_tree_frame_sink_state_ = LayerTreeFrameSinkState::CREATING;
 
   // The following DCHECKs make sure we are in the proper quiescent state.
   // The pipeline should be flushed entirely before we start output
   // surface creation to avoid complicated corner cases.
-  DCHECK(begin_main_frame_state_ == BEGIN_MAIN_FRAME_STATE_IDLE);
+  DCHECK(begin_main_frame_state_ == BeginMainFrameState::IDLE);
   DCHECK(!has_pending_tree_);
   DCHECK(!active_tree_needs_first_draw_);
 }
@@ -950,7 +953,7 @@
 bool SchedulerStateMachine::BeginFrameRequiredForAction() const {
   // The forced draw respects our normal draw scheduling, so we need to
   // request a BeginImplFrame for it.
-  if (forced_redraw_state_ == FORCED_REDRAW_STATE_WAITING_FOR_DRAW)
+  if (forced_redraw_state_ == ForcedRedrawOnTimeoutState::WAITING_FOR_DRAW)
     return true;
 
   return needs_redraw_ || needs_one_begin_impl_frame_ ||
@@ -974,8 +977,7 @@
   // request frames when commits are disabled, because the frame requests will
   // not provide the needed commit (and will wake up the process when it could
   // stay idle).
-  if ((begin_main_frame_state_ != BEGIN_MAIN_FRAME_STATE_IDLE) &&
-      !defer_commits_)
+  if ((begin_main_frame_state_ != BeginMainFrameState::IDLE) && !defer_commits_)
     return true;
 
   // If the pending tree activates quickly, we'll want a BeginImplFrame soon
@@ -1005,7 +1007,7 @@
 
 void SchedulerStateMachine::OnBeginImplFrame(uint64_t source_id,
                                              uint64_t sequence_number) {
-  begin_impl_frame_state_ = BEGIN_IMPL_FRAME_STATE_INSIDE_BEGIN_FRAME;
+  begin_impl_frame_state_ = BeginImplFrameState::INSIDE_BEGIN_FRAME;
   current_frame_number_++;
 
   // Cache the values from the previous impl frame before reseting them for this
@@ -1026,14 +1028,14 @@
 }
 
 void SchedulerStateMachine::OnBeginImplFrameDeadline() {
-  begin_impl_frame_state_ = BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE;
+  begin_impl_frame_state_ = BeginImplFrameState::INSIDE_DEADLINE;
 
   // Clear funnels for any actions we perform during the deadline.
   did_draw_ = false;
 }
 
 void SchedulerStateMachine::OnBeginImplFrameIdle() {
-  begin_impl_frame_state_ = BEGIN_IMPL_FRAME_STATE_IDLE;
+  begin_impl_frame_state_ = BeginImplFrameState::IDLE;
 
   // Count any prepare tiles that happens in commits in between frames. We want
   // to prevent a prepare tiles during the next frame's deadline in that case.
@@ -1058,19 +1060,19 @@
 SchedulerStateMachine::CurrentBeginImplFrameDeadlineMode() const {
   if (settings_.using_synchronous_renderer_compositor) {
     // No deadline for synchronous compositor.
-    return BEGIN_IMPL_FRAME_DEADLINE_MODE_NONE;
+    return BeginImplFrameDeadlineMode::NONE;
   } else if (ShouldBlockDeadlineIndefinitely()) {
-    return BEGIN_IMPL_FRAME_DEADLINE_MODE_BLOCKED;
+    return BeginImplFrameDeadlineMode::BLOCKED;
   } else if (ShouldTriggerBeginImplFrameDeadlineImmediately()) {
-    return BEGIN_IMPL_FRAME_DEADLINE_MODE_IMMEDIATE;
+    return BeginImplFrameDeadlineMode::IMMEDIATE;
   } else if (needs_redraw_) {
     // We have an animation or fast input path on the impl thread that wants
     // to draw, so don't wait too long for a new active tree.
-    return BEGIN_IMPL_FRAME_DEADLINE_MODE_REGULAR;
+    return BeginImplFrameDeadlineMode::REGULAR;
   } else {
     // The impl thread doesn't have anything it wants to draw and we are just
     // waiting for a new active tree. In short we are blocked.
-    return BEGIN_IMPL_FRAME_DEADLINE_MODE_LATE;
+    return BeginImplFrameDeadlineMode::LATE;
   }
 }
 
@@ -1119,7 +1121,7 @@
 
   // Avoid blocking for any reason if we don't have a layer tree frame sink or
   // are invisible.
-  if (layer_tree_frame_sink_state_ == LAYER_TREE_FRAME_SINK_NONE)
+  if (layer_tree_frame_sink_state_ == LayerTreeFrameSinkState::NONE)
     return false;
 
   if (!visible_)
@@ -1136,7 +1138,7 @@
   if (ShouldSendBeginMainFrame())
     return true;
 
-  if (begin_main_frame_state_ != BEGIN_MAIN_FRAME_STATE_IDLE)
+  if (begin_main_frame_state_ != BeginMainFrameState::IDLE)
     return true;
 
   // Wait for tiles and activation.
@@ -1193,8 +1195,7 @@
   bool has_impl_updates = needs_redraw_ || needs_one_begin_impl_frame_;
   bool main_updates_expected =
       needs_begin_main_frame_ ||
-      begin_main_frame_state_ != BEGIN_MAIN_FRAME_STATE_IDLE ||
-      has_pending_tree_;
+      begin_main_frame_state_ != BeginMainFrameState::IDLE || has_pending_tree_;
   return has_impl_updates && !main_updates_expected;
 }
 
@@ -1258,9 +1259,9 @@
 }
 
 void SchedulerStateMachine::NotifyReadyToCommit() {
-  DCHECK_EQ(begin_main_frame_state_, BEGIN_MAIN_FRAME_STATE_STARTED)
+  DCHECK_EQ(begin_main_frame_state_, BeginMainFrameState::STARTED)
       << AsValue()->ToString();
-  begin_main_frame_state_ = BEGIN_MAIN_FRAME_STATE_READY_TO_COMMIT;
+  begin_main_frame_state_ = BeginMainFrameState::READY_TO_COMMIT;
   // In commit_to_active_tree mode, commit should happen right after BeginFrame,
   // meaning when this function is called, next action should be commit.
   if (settings_.commit_to_active_tree)
@@ -1268,7 +1269,7 @@
 }
 
 void SchedulerStateMachine::BeginMainFrameAborted(CommitEarlyOutReason reason) {
-  DCHECK_EQ(begin_main_frame_state_, BEGIN_MAIN_FRAME_STATE_STARTED);
+  DCHECK_EQ(begin_main_frame_state_, BeginMainFrameState::STARTED);
 
   // If the main thread aborted, it doesn't matter if the  main thread missed
   // the last deadline since it didn't have an update anyway.
@@ -1278,7 +1279,7 @@
     case CommitEarlyOutReason::ABORTED_LAYER_TREE_FRAME_SINK_LOST:
     case CommitEarlyOutReason::ABORTED_NOT_VISIBLE:
     case CommitEarlyOutReason::ABORTED_DEFERRED_COMMIT:
-      begin_main_frame_state_ = BEGIN_MAIN_FRAME_STATE_IDLE;
+      begin_main_frame_state_ = BeginMainFrameState::IDLE;
       SetNeedsBeginMainFrame();
       return;
     case CommitEarlyOutReason::FINISHED_NO_UPDATES:
@@ -1294,10 +1295,10 @@
 }
 
 void SchedulerStateMachine::DidLoseLayerTreeFrameSink() {
-  if (layer_tree_frame_sink_state_ == LAYER_TREE_FRAME_SINK_NONE ||
-      layer_tree_frame_sink_state_ == LAYER_TREE_FRAME_SINK_CREATING)
+  if (layer_tree_frame_sink_state_ == LayerTreeFrameSinkState::NONE ||
+      layer_tree_frame_sink_state_ == LayerTreeFrameSinkState::CREATING)
     return;
-  layer_tree_frame_sink_state_ = LAYER_TREE_FRAME_SINK_NONE;
+  layer_tree_frame_sink_state_ = LayerTreeFrameSinkState::NONE;
   needs_redraw_ = false;
 }
 
@@ -1314,8 +1315,9 @@
 }
 
 void SchedulerStateMachine::DidCreateAndInitializeLayerTreeFrameSink() {
-  DCHECK_EQ(layer_tree_frame_sink_state_, LAYER_TREE_FRAME_SINK_CREATING);
-  layer_tree_frame_sink_state_ = LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_COMMIT;
+  DCHECK_EQ(layer_tree_frame_sink_state_, LayerTreeFrameSinkState::CREATING);
+  layer_tree_frame_sink_state_ =
+      LayerTreeFrameSinkState::WAITING_FOR_FIRST_COMMIT;
 
   if (did_create_and_initialize_first_layer_tree_frame_sink_) {
     // TODO(boliu): See if we can remove this when impl-side painting is always
@@ -1329,19 +1331,19 @@
 }
 
 void SchedulerStateMachine::NotifyBeginMainFrameStarted() {
-  DCHECK_EQ(begin_main_frame_state_, BEGIN_MAIN_FRAME_STATE_SENT);
-  begin_main_frame_state_ = BEGIN_MAIN_FRAME_STATE_STARTED;
+  DCHECK_EQ(begin_main_frame_state_, BeginMainFrameState::SENT);
+  begin_main_frame_state_ = BeginMainFrameState::STARTED;
 }
 
 bool SchedulerStateMachine::HasInitializedLayerTreeFrameSink() const {
   switch (layer_tree_frame_sink_state_) {
-    case LAYER_TREE_FRAME_SINK_NONE:
-    case LAYER_TREE_FRAME_SINK_CREATING:
+    case LayerTreeFrameSinkState::NONE:
+    case LayerTreeFrameSinkState::CREATING:
       return false;
 
-    case LAYER_TREE_FRAME_SINK_ACTIVE:
-    case LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_COMMIT:
-    case LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_ACTIVATION:
+    case LayerTreeFrameSinkState::ACTIVE:
+    case LayerTreeFrameSinkState::WAITING_FOR_FIRST_COMMIT:
+    case LayerTreeFrameSinkState::WAITING_FOR_FIRST_ACTIVATION:
       return true;
   }
   NOTREACHED();
diff --git a/cc/scheduler/scheduler_state_machine.h b/cc/scheduler/scheduler_state_machine.h
index c0f1f0a..4e2f664 100644
--- a/cc/scheduler/scheduler_state_machine.h
+++ b/cc/scheduler/scheduler_state_machine.h
@@ -50,48 +50,48 @@
   explicit SchedulerStateMachine(const SchedulerSettings& settings);
   ~SchedulerStateMachine();
 
-  enum LayerTreeFrameSinkState {
-    LAYER_TREE_FRAME_SINK_NONE,
-    LAYER_TREE_FRAME_SINK_ACTIVE,
-    LAYER_TREE_FRAME_SINK_CREATING,
-    LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_COMMIT,
-    LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_ACTIVATION,
+  enum class LayerTreeFrameSinkState {
+    NONE,
+    ACTIVE,
+    CREATING,
+    WAITING_FOR_FIRST_COMMIT,
+    WAITING_FOR_FIRST_ACTIVATION,
   };
   static const char* LayerTreeFrameSinkStateToString(
       LayerTreeFrameSinkState state);
 
   // Note: BeginImplFrameState does not cycle through these states in a fixed
   // order on all platforms. It's up to the scheduler to set these correctly.
-  enum BeginImplFrameState {
-    BEGIN_IMPL_FRAME_STATE_IDLE,
-    BEGIN_IMPL_FRAME_STATE_INSIDE_BEGIN_FRAME,
-    BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE,
+  enum class BeginImplFrameState {
+    IDLE,
+    INSIDE_BEGIN_FRAME,
+    INSIDE_DEADLINE,
   };
   static const char* BeginImplFrameStateToString(BeginImplFrameState state);
 
-  enum BeginImplFrameDeadlineMode {
-    BEGIN_IMPL_FRAME_DEADLINE_MODE_NONE,
-    BEGIN_IMPL_FRAME_DEADLINE_MODE_IMMEDIATE,
-    BEGIN_IMPL_FRAME_DEADLINE_MODE_REGULAR,
-    BEGIN_IMPL_FRAME_DEADLINE_MODE_LATE,
-    BEGIN_IMPL_FRAME_DEADLINE_MODE_BLOCKED,
+  enum class BeginImplFrameDeadlineMode {
+    NONE,
+    IMMEDIATE,
+    REGULAR,
+    LATE,
+    BLOCKED,
   };
   static const char* BeginImplFrameDeadlineModeToString(
       BeginImplFrameDeadlineMode mode);
 
-  enum BeginMainFrameState {
-    BEGIN_MAIN_FRAME_STATE_IDLE,
-    BEGIN_MAIN_FRAME_STATE_SENT,
-    BEGIN_MAIN_FRAME_STATE_STARTED,
-    BEGIN_MAIN_FRAME_STATE_READY_TO_COMMIT,
+  enum class BeginMainFrameState {
+    IDLE,
+    SENT,
+    STARTED,
+    READY_TO_COMMIT,
   };
   static const char* BeginMainFrameStateToString(BeginMainFrameState state);
 
-  enum ForcedRedrawOnTimeoutState {
-    FORCED_REDRAW_STATE_IDLE,
-    FORCED_REDRAW_STATE_WAITING_FOR_COMMIT,
-    FORCED_REDRAW_STATE_WAITING_FOR_ACTIVATION,
-    FORCED_REDRAW_STATE_WAITING_FOR_DRAW,
+  enum class ForcedRedrawOnTimeoutState {
+    IDLE,
+    WAITING_FOR_COMMIT,
+    WAITING_FOR_ACTIVATION,
+    WAITING_FOR_DRAW,
   };
   static const char* ForcedRedrawOnTimeoutStateToString(
       ForcedRedrawOnTimeoutState state);
@@ -101,9 +101,9 @@
   }
 
   bool CommitPending() const {
-    return begin_main_frame_state_ == BEGIN_MAIN_FRAME_STATE_SENT ||
-           begin_main_frame_state_ == BEGIN_MAIN_FRAME_STATE_STARTED ||
-           begin_main_frame_state_ == BEGIN_MAIN_FRAME_STATE_READY_TO_COMMIT;
+    return begin_main_frame_state_ == BeginMainFrameState::SENT ||
+           begin_main_frame_state_ == BeginMainFrameState::STARTED ||
+           begin_main_frame_state_ == BeginMainFrameState::READY_TO_COMMIT;
   }
 
   bool NewActiveTreeLikely() const {
@@ -113,19 +113,19 @@
   bool RedrawPending() const { return needs_redraw_; }
   bool PrepareTilesPending() const { return needs_prepare_tiles_; }
 
-  enum Action {
-    ACTION_NONE,
-    ACTION_SEND_BEGIN_MAIN_FRAME,
-    ACTION_COMMIT,
-    ACTION_ACTIVATE_SYNC_TREE,
-    ACTION_PERFORM_IMPL_SIDE_INVALIDATION,
-    ACTION_DRAW_IF_POSSIBLE,
-    ACTION_DRAW_FORCED,
-    ACTION_DRAW_ABORT,
-    ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION,
-    ACTION_PREPARE_TILES,
-    ACTION_INVALIDATE_LAYER_TREE_FRAME_SINK,
-    ACTION_NOTIFY_BEGIN_MAIN_FRAME_NOT_SENT,
+  enum class Action {
+    NONE,
+    SEND_BEGIN_MAIN_FRAME,
+    COMMIT,
+    ACTIVATE_SYNC_TREE,
+    PERFORM_IMPL_SIDE_INVALIDATION,
+    DRAW_IF_POSSIBLE,
+    DRAW_FORCED,
+    DRAW_ABORT,
+    BEGIN_LAYER_TREE_FRAME_SINK_CREATION,
+    PREPARE_TILES,
+    INVALIDATE_LAYER_TREE_FRAME_SINK,
+    NOTIFY_BEGIN_MAIN_FRAME_NOT_SENT,
   };
   static const char* ActionToString(Action action);
 
@@ -232,12 +232,12 @@
   // active).
   void SetNeedsOneBeginImplFrame();
 
-  // Call this only in response to receiving an ACTION_SEND_BEGIN_MAIN_FRAME
+  // Call this only in response to receiving an Action::SEND_BEGIN_MAIN_FRAME
   // from NextAction.
   // Indicates that all painting is complete.
   void NotifyReadyToCommit();
 
-  // Call this only in response to receiving an ACTION_SEND_BEGIN_MAIN_FRAME
+  // Call this only in response to receiving an Action::SEND_BEGIN_MAIN_FRAME
   // from NextAction if the client rejects the BeginMainFrame message.
   void BeginMainFrameAborted(CommitEarlyOutReason reason);
 
@@ -324,10 +324,11 @@
   const SchedulerSettings settings_;
 
   LayerTreeFrameSinkState layer_tree_frame_sink_state_ =
-      LAYER_TREE_FRAME_SINK_NONE;
-  BeginImplFrameState begin_impl_frame_state_ = BEGIN_IMPL_FRAME_STATE_IDLE;
-  BeginMainFrameState begin_main_frame_state_ = BEGIN_MAIN_FRAME_STATE_IDLE;
-  ForcedRedrawOnTimeoutState forced_redraw_state_ = FORCED_REDRAW_STATE_IDLE;
+      LayerTreeFrameSinkState::NONE;
+  BeginImplFrameState begin_impl_frame_state_ = BeginImplFrameState::IDLE;
+  BeginMainFrameState begin_main_frame_state_ = BeginMainFrameState::IDLE;
+  ForcedRedrawOnTimeoutState forced_redraw_state_ =
+      ForcedRedrawOnTimeoutState::IDLE;
 
   // These are used for tracing only.
   int commit_count_ = 0;
diff --git a/cc/scheduler/scheduler_state_machine_unittest.cc b/cc/scheduler/scheduler_state_machine_unittest.cc
index c670aa7..ed089d15 100644
--- a/cc/scheduler/scheduler_state_machine_unittest.cc
+++ b/cc/scheduler/scheduler_state_machine_unittest.cc
@@ -36,26 +36,26 @@
   EXPECT_ENUM_EQ(ActionToString, expected, state.NextAction()) \
       << state.AsValue()->ToString()
 
-#define EXPECT_ACTION_UPDATE_STATE(action)                              \
-  EXPECT_ACTION(action);                                                \
-  if (action == SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE ||       \
-      action == SchedulerStateMachine::ACTION_DRAW_FORCED) {            \
-    EXPECT_IMPL_FRAME_STATE(                                            \
-        SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE); \
-  }                                                                     \
-  PerformAction(&state, action);                                        \
-  if (action == SchedulerStateMachine::ACTION_NONE) {                   \
-    if (state.begin_impl_frame_state() ==                               \
-        SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE)  \
-      state.OnBeginImplFrameIdle();                                     \
+#define EXPECT_ACTION_UPDATE_STATE(action)                            \
+  EXPECT_ACTION(action);                                              \
+  if (action == SchedulerStateMachine::Action::DRAW_IF_POSSIBLE ||    \
+      action == SchedulerStateMachine::Action::DRAW_FORCED) {         \
+    EXPECT_IMPL_FRAME_STATE(                                          \
+        SchedulerStateMachine::BeginImplFrameState::INSIDE_DEADLINE); \
+  }                                                                   \
+  PerformAction(&state, action);                                      \
+  if (action == SchedulerStateMachine::Action::NONE) {                \
+    if (state.begin_impl_frame_state() ==                             \
+        SchedulerStateMachine::BeginImplFrameState::INSIDE_DEADLINE)  \
+      state.OnBeginImplFrameIdle();                                   \
   }
 
-#define SET_UP_STATE(state)                                                \
-  state.SetVisible(true);                                                  \
-  EXPECT_ACTION_UPDATE_STATE(                                              \
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION); \
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);          \
-  state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();        \
+#define SET_UP_STATE(state)                                                 \
+  state.SetVisible(true);                                                   \
+  EXPECT_ACTION_UPDATE_STATE(                                               \
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION); \
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);          \
+  state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();         \
   state.SetCanDraw(true);
 
 namespace cc {
@@ -64,16 +64,16 @@
 
 const SchedulerStateMachine::BeginImplFrameState all_begin_impl_frame_states[] =
     {
-        SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_IDLE,
-        SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_BEGIN_FRAME,
-        SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE,
+        SchedulerStateMachine::BeginImplFrameState::IDLE,
+        SchedulerStateMachine::BeginImplFrameState::INSIDE_BEGIN_FRAME,
+        SchedulerStateMachine::BeginImplFrameState::INSIDE_DEADLINE,
 };
 
 const SchedulerStateMachine::BeginMainFrameState begin_main_frame_states[] = {
-    SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE,
-    SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT,
-    SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_STARTED,
-    SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_READY_TO_COMMIT};
+    SchedulerStateMachine::BeginMainFrameState::IDLE,
+    SchedulerStateMachine::BeginMainFrameState::SENT,
+    SchedulerStateMachine::BeginMainFrameState::STARTED,
+    SchedulerStateMachine::BeginMainFrameState::READY_TO_COMMIT};
 
 // Exposes the protected state fields of the SchedulerStateMachine for testing
 class StateMachine : public SchedulerStateMachine {
@@ -84,7 +84,7 @@
 
   void CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit() {
     DidCreateAndInitializeLayerTreeFrameSink();
-    layer_tree_frame_sink_state_ = LAYER_TREE_FRAME_SINK_ACTIVE;
+    layer_tree_frame_sink_state_ = LayerTreeFrameSinkState::ACTIVE;
   }
 
   void IssueNextBeginImplFrame() {
@@ -132,11 +132,11 @@
   DrawResult draw_result_for_test() { return draw_result_for_test_; }
 
   void SetNeedsForcedRedrawForTimeout(bool b) {
-    forced_redraw_state_ = FORCED_REDRAW_STATE_WAITING_FOR_COMMIT;
+    forced_redraw_state_ = ForcedRedrawOnTimeoutState::WAITING_FOR_COMMIT;
     active_tree_needs_first_draw_ = true;
   }
   bool NeedsForcedRedrawForTimeout() const {
-    return forced_redraw_state_ != FORCED_REDRAW_STATE_IDLE;
+    return forced_redraw_state_ != ForcedRedrawOnTimeoutState::IDLE;
   }
 
   void SetActiveTreeNeedsFirstDraw(bool needs_first_draw) {
@@ -171,52 +171,52 @@
 
 void PerformAction(StateMachine* sm, SchedulerStateMachine::Action action) {
   switch (action) {
-    case SchedulerStateMachine::ACTION_NONE:
+    case SchedulerStateMachine::Action::NONE:
       return;
 
-    case SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE:
+    case SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE:
       sm->WillActivate();
       return;
 
-    case SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME:
+    case SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME:
       sm->WillSendBeginMainFrame();
       return;
 
-    case SchedulerStateMachine::ACTION_NOTIFY_BEGIN_MAIN_FRAME_NOT_SENT:
+    case SchedulerStateMachine::Action::NOTIFY_BEGIN_MAIN_FRAME_NOT_SENT:
       sm->WillNotifyBeginMainFrameNotSent();
       return;
 
-    case SchedulerStateMachine::ACTION_COMMIT: {
+    case SchedulerStateMachine::Action::COMMIT: {
       bool commit_has_no_updates = false;
       sm->WillCommit(commit_has_no_updates);
       return;
     }
 
-    case SchedulerStateMachine::ACTION_DRAW_FORCED:
-    case SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE: {
+    case SchedulerStateMachine::Action::DRAW_FORCED:
+    case SchedulerStateMachine::Action::DRAW_IF_POSSIBLE: {
       sm->WillDraw();
       sm->DidDraw(sm->draw_result_for_test());
       return;
     }
 
-    case SchedulerStateMachine::ACTION_DRAW_ABORT: {
+    case SchedulerStateMachine::Action::DRAW_ABORT: {
       sm->AbortDraw();
       return;
     }
 
-    case SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION:
+    case SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION:
       sm->WillBeginLayerTreeFrameSinkCreation();
       return;
 
-    case SchedulerStateMachine::ACTION_PREPARE_TILES:
+    case SchedulerStateMachine::Action::PREPARE_TILES:
       sm->WillPrepareTiles();
       return;
 
-    case SchedulerStateMachine::ACTION_INVALIDATE_LAYER_TREE_FRAME_SINK:
+    case SchedulerStateMachine::Action::INVALIDATE_LAYER_TREE_FRAME_SINK:
       sm->WillInvalidateLayerTreeFrameSink();
       return;
 
-    case SchedulerStateMachine::ACTION_PERFORM_IMPL_SIDE_INVALIDATION:
+    case SchedulerStateMachine::Action::PERFORM_IMPL_SIDE_INVALIDATION:
       sm->WillPerformImplSideInvalidation();
       return;
   }
@@ -227,11 +227,11 @@
   StateMachine state(default_scheduler_settings);
   state.SetVisible(true);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
   state.SetBeginMainFrameState(
-      SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
+      SchedulerStateMachine::BeginMainFrameState::IDLE);
 
   // Don't request BeginFrames if we are idle.
   state.SetNeedsRedraw(false);
@@ -278,18 +278,18 @@
   state.SetMainThreadWantsBeginMainFrameNotExpectedMessages(true);
   state.SetVisible(true);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
   state.IssueNextBeginImplFrame();
   state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_NOTIFY_BEGIN_MAIN_FRAME_NOT_SENT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::NOTIFY_BEGIN_MAIN_FRAME_NOT_SENT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   state.SetNeedsRedraw(true);
   state.SetNeedsBeginMainFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest, TestNextActionBeginsMainFrameIfNeeded) {
@@ -300,21 +300,21 @@
     StateMachine state(default_scheduler_settings);
     state.SetVisible(true);
     EXPECT_ACTION_UPDATE_STATE(
-        SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+        SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
     state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
     state.SetBeginMainFrameState(
-        SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
+        SchedulerStateMachine::BeginMainFrameState::IDLE);
     state.SetNeedsRedraw(false);
 
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
     EXPECT_FALSE(state.NeedsCommit());
 
     state.IssueNextBeginImplFrame();
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
     state.OnBeginImplFrameDeadline();
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
     EXPECT_FALSE(state.NeedsCommit());
   }
 
@@ -322,18 +322,18 @@
   {
     StateMachine state(default_scheduler_settings);
     state.SetBeginMainFrameState(
-        SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
+        SchedulerStateMachine::BeginMainFrameState::IDLE);
     state.SetNeedsRedraw(false);
     state.SetNeedsBeginMainFrame();
 
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
     EXPECT_TRUE(state.NeedsCommit());
 
     state.IssueNextBeginImplFrame();
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
     state.OnBeginImplFrameDeadline();
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
     EXPECT_TRUE(state.NeedsCommit());
   }
 
@@ -341,26 +341,26 @@
   {
     StateMachine state(default_scheduler_settings);
     state.SetBeginMainFrameState(
-        SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
+        SchedulerStateMachine::BeginMainFrameState::IDLE);
     state.SetVisible(true);
     EXPECT_ACTION_UPDATE_STATE(
-        SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+        SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
     state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
     state.SetNeedsRedraw(false);
     state.SetNeedsBeginMainFrame();
 
     // Expect nothing to happen until after OnBeginImplFrame.
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
-    EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
-    EXPECT_IMPL_FRAME_STATE(SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_IDLE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
+    EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::IDLE);
+    EXPECT_IMPL_FRAME_STATE(SchedulerStateMachine::BeginImplFrameState::IDLE);
     EXPECT_TRUE(state.NeedsCommit());
     EXPECT_TRUE(state.BeginFrameNeeded());
 
     state.IssueNextBeginImplFrame();
     EXPECT_ACTION_UPDATE_STATE(
-        SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-    EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT);
+        SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+    EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::SENT);
     EXPECT_FALSE(state.NeedsCommit());
   }
 
@@ -368,26 +368,26 @@
   {
     StateMachine state(default_scheduler_settings);
     state.SetBeginMainFrameState(
-        SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
+        SchedulerStateMachine::BeginMainFrameState::IDLE);
     state.SetVisible(true);
     EXPECT_ACTION_UPDATE_STATE(
-        SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+        SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
     state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
     state.SetNeedsRedraw(false);
     state.SetNeedsBeginMainFrame();
     state.SetCanDraw(false);
 
     // Expect nothing to happen until after OnBeginImplFrame.
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
-    EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
-    EXPECT_IMPL_FRAME_STATE(SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_IDLE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
+    EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::IDLE);
+    EXPECT_IMPL_FRAME_STATE(SchedulerStateMachine::BeginImplFrameState::IDLE);
     EXPECT_TRUE(state.BeginFrameNeeded());
 
     state.IssueNextBeginImplFrame();
     EXPECT_ACTION_UPDATE_STATE(
-        SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-    EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT);
+        SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+    EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::SENT);
     EXPECT_FALSE(state.NeedsCommit());
   }
 }
@@ -398,7 +398,7 @@
   scheduler_settings.main_frame_before_activation_enabled = true;
   StateMachine state(scheduler_settings);
   state.SetBeginMainFrameState(
-      SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
+      SchedulerStateMachine::BeginMainFrameState::IDLE);
   SET_UP_STATE(state)
   state.SetNeedsRedraw(false);
   state.SetNeedsBeginMainFrame();
@@ -408,45 +408,45 @@
   // Commit to the pending tree.
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::IDLE);
 
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Verify that the next commit starts while there is still a pending tree.
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Verify the pending commit doesn't overwrite the pending
   // tree until the pending tree has been activated.
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Verify NotifyReadyToActivate unblocks activation, commit, and
   // draw in that order.
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   EXPECT_TRUE(state.ShouldTriggerBeginImplFrameDeadlineImmediately());
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
   state.DidReceiveCompositorFrameAck();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::IDLE);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -460,31 +460,31 @@
 
   // Start a frame.
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_FALSE(state.CommitPending());
 
   // Failing a draw triggers request for a new BeginMainFrame.
   state.OnBeginImplFrameDeadline();
   state.SetDrawResultForTest(DRAW_ABORTED_CHECKERBOARD_ANIMATIONS);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameIdle();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // It's okay to attempt more draws just in case additional raster
   // finishes and the requested commit wasn't actually necessary.
   EXPECT_TRUE(state.CommitPending());
   EXPECT_TRUE(state.RedrawPending());
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
   state.SetDrawResultForTest(DRAW_ABORTED_CHECKERBOARD_ANIMATIONS);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameIdle();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest, FailedDrawForMissingHighResNeedsCommit) {
@@ -497,50 +497,50 @@
 
   // Start a frame.
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_FALSE(state.CommitPending());
 
   // Failing a draw triggers because of high res tiles missing
   // request for a new BeginMainFrame.
   state.OnBeginImplFrameDeadline();
   state.SetDrawResultForTest(DRAW_ABORTED_MISSING_HIGH_RES_CONTENT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameIdle();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // It doesn't request a draw until we get a new commit though.
   EXPECT_TRUE(state.CommitPending());
   EXPECT_FALSE(state.RedrawPending());
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameIdle();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Finish the commit and activation.
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.RedrawPending());
 
   // Verify we draw with the new frame.
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
   state.SetDrawResultForTest(DRAW_SUCCESS);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameIdle();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -552,19 +552,19 @@
 
   // Start a commit.
   state.SetNeedsBeginMainFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.CommitPending());
 
   // Then initiate a draw that fails.
   state.SetNeedsRedraw(true);
   state.OnBeginImplFrameDeadline();
   state.SetDrawResultForTest(DRAW_ABORTED_CHECKERBOARD_ANIMATIONS);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.BeginFrameNeeded());
   EXPECT_TRUE(state.RedrawPending());
   EXPECT_TRUE(state.CommitPending());
@@ -573,28 +573,28 @@
   // continue the commit as usual.
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.RedrawPending());
 
   // Activate so we're ready for a new main frame.
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.RedrawPending());
 
   // The redraw should be forced at the end of the next BeginImplFrame.
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
   state.SetDrawResultForTest(DRAW_SUCCESS);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_FORCED);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_FORCED);
   state.DidSubmitCompositorFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.DidReceiveCompositorFrameAck();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest, TestFailedDrawsDoNotRestartForcedDraw) {
@@ -607,54 +607,56 @@
 
   // Start a commit.
   state.SetNeedsBeginMainFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.CommitPending());
 
   // Then initiate a draw.
   state.SetNeedsRedraw(true);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
 
   // Fail the draw enough times to force a redraw.
   for (int i = 0; i < draw_limit; ++i) {
     state.SetNeedsRedraw(true);
     state.IssueNextBeginImplFrame();
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
     state.OnBeginImplFrameDeadline();
     state.SetDrawResultForTest(DRAW_ABORTED_CHECKERBOARD_ANIMATIONS);
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
     state.OnBeginImplFrameIdle();
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   }
 
   EXPECT_TRUE(state.BeginFrameNeeded());
   EXPECT_TRUE(state.RedrawPending());
   // But the commit is ongoing.
   EXPECT_TRUE(state.CommitPending());
-  EXPECT_TRUE(state.ForcedRedrawState() ==
-              SchedulerStateMachine::FORCED_REDRAW_STATE_WAITING_FOR_COMMIT);
+  EXPECT_TRUE(
+      state.ForcedRedrawState() ==
+      SchedulerStateMachine::ForcedRedrawOnTimeoutState::WAITING_FOR_COMMIT);
 
   // After failing additional draws, we should still be in a forced
   // redraw, but not back in IDLE.
   for (int i = 0; i < draw_limit; ++i) {
     state.SetNeedsRedraw(true);
     state.IssueNextBeginImplFrame();
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
     state.OnBeginImplFrameDeadline();
     state.SetDrawResultForTest(DRAW_ABORTED_CHECKERBOARD_ANIMATIONS);
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
     state.OnBeginImplFrameIdle();
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   }
   EXPECT_TRUE(state.RedrawPending());
-  EXPECT_TRUE(state.ForcedRedrawState() ==
-              SchedulerStateMachine::FORCED_REDRAW_STATE_WAITING_FOR_COMMIT);
+  EXPECT_TRUE(
+      state.ForcedRedrawState() ==
+      SchedulerStateMachine::ForcedRedrawOnTimeoutState::WAITING_FOR_COMMIT);
 }
 
 TEST(SchedulerStateMachineTest, TestFailedDrawIsRetriedInNextBeginImplFrame) {
@@ -666,31 +668,31 @@
   state.SetNeedsRedraw(true);
   EXPECT_TRUE(state.BeginFrameNeeded());
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
   EXPECT_TRUE(state.RedrawPending());
   state.SetDrawResultForTest(DRAW_ABORTED_CHECKERBOARD_ANIMATIONS);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
 
   // Failing the draw for animation checkerboards makes us require a commit.
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.RedrawPending());
 
   // We should not be trying to draw again now, but we have a commit pending.
   EXPECT_TRUE(state.BeginFrameNeeded());
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // We should try to draw again at the end of the next BeginImplFrame on
   // the impl thread.
   state.OnBeginImplFrameDeadline();
   state.SetDrawResultForTest(DRAW_SUCCESS);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
   state.DidReceiveCompositorFrameAck();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest, TestDoestDrawTwiceInSameFrame) {
@@ -702,28 +704,28 @@
   // Draw the first frame.
   EXPECT_TRUE(state.BeginFrameNeeded());
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
   state.DidReceiveCompositorFrameAck();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Before the next BeginImplFrame, set needs redraw again.
   // This should not redraw until the next BeginImplFrame.
   state.SetNeedsRedraw(true);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Move to another frame. This should now draw.
   EXPECT_TRUE(state.BeginFrameNeeded());
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
   state.DidReceiveCompositorFrameAck();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // We just submitted, so we should proactively request another BeginImplFrame.
   EXPECT_TRUE(state.BeginFrameNeeded());
@@ -745,23 +747,23 @@
       StateMachine state(default_scheduler_settings);
       state.SetVisible(true);
       EXPECT_ACTION_UPDATE_STATE(
-          SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-      EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+          SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+      EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
       state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
       state.SetBeginMainFrameState(begin_main_frame_states[i]);
       state.SetBeginImplFrameState(all_begin_impl_frame_states[j]);
       bool visible =
           (all_begin_impl_frame_states[j] !=
-           SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE);
+           SchedulerStateMachine::BeginImplFrameState::INSIDE_DEADLINE);
       state.SetVisible(visible);
 
       // Case 1: needs_begin_main_frame=false
-      EXPECT_NE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE,
+      EXPECT_NE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE,
                 state.NextAction());
 
       // Case 2: needs_begin_main_frame=true
       state.SetNeedsBeginMainFrame();
-      EXPECT_NE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE,
+      EXPECT_NE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE,
                 state.NextAction())
           << state.AsValue()->ToString();
     }
@@ -773,22 +775,22 @@
     StateMachine state(default_scheduler_settings);
     state.SetVisible(true);
     EXPECT_ACTION_UPDATE_STATE(
-        SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+        SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+    EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
     state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
     state.SetCanDraw(true);
     state.SetBeginMainFrameState(begin_main_frame_states[i]);
     state.SetBeginImplFrameState(
-        SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE);
+        SchedulerStateMachine::BeginImplFrameState::INSIDE_DEADLINE);
 
     state.SetNeedsRedraw(true);
 
     SchedulerStateMachine::Action expected_action;
     if (begin_main_frame_states[i] ==
-        SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_READY_TO_COMMIT) {
-      expected_action = SchedulerStateMachine::ACTION_COMMIT;
+        SchedulerStateMachine::BeginMainFrameState::READY_TO_COMMIT) {
+      expected_action = SchedulerStateMachine::Action::COMMIT;
     } else {
-      expected_action = SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE;
+      expected_action = SchedulerStateMachine::Action::DRAW_IF_POSSIBLE;
     }
 
     // Case 1: needs_begin_main_frame=false.
@@ -812,24 +814,24 @@
       StateMachine state(default_scheduler_settings);
       state.SetVisible(true);
       EXPECT_ACTION_UPDATE_STATE(
-          SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-      EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+          SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+      EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
       state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
       state.SetBeginMainFrameState(begin_main_frame_states[i]);
       state.SetVisible(false);
       state.SetNeedsRedraw(true);
       if (j == 1) {
         state.SetBeginImplFrameState(
-            SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE);
+            SchedulerStateMachine::BeginImplFrameState::INSIDE_DEADLINE);
       }
 
       // Case 1: needs_begin_main_frame=false.
-      EXPECT_NE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE,
+      EXPECT_NE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE,
                 state.NextAction());
 
       // Case 2: needs_begin_main_frame=true.
       state.SetNeedsBeginMainFrame();
-      EXPECT_NE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE,
+      EXPECT_NE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE,
                 state.NextAction())
           << state.AsValue()->ToString();
     }
@@ -848,8 +850,8 @@
       StateMachine state(default_scheduler_settings);
       state.SetVisible(true);
       EXPECT_ACTION_UPDATE_STATE(
-          SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-      EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+          SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+      EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
       state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
       state.SetBeginMainFrameState(begin_main_frame_states[i]);
       state.SetVisible(false);
@@ -858,7 +860,7 @@
         state.IssueNextBeginImplFrame();
 
       state.SetCanDraw(false);
-      EXPECT_NE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE,
+      EXPECT_NE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE,
                 state.NextAction());
     }
   }
@@ -870,8 +872,8 @@
   StateMachine state(default_scheduler_settings);
   state.SetVisible(true);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
 
   state.SetActiveTreeNeedsFirstDraw(true);
@@ -879,18 +881,18 @@
   state.SetNeedsRedraw(true);
   state.SetCanDraw(false);
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_ABORT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_ABORT);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_ABORT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_ABORT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest, TestSetNeedsBeginMainFrameIsNotLost) {
@@ -904,8 +906,8 @@
   // Begin the frame.
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::SENT);
 
   // Now, while the frame is in progress, set another commit.
   state.SetNeedsBeginMainFrame();
@@ -915,43 +917,43 @@
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
   EXPECT_MAIN_FRAME_STATE(
-      SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_READY_TO_COMMIT);
+      SchedulerStateMachine::BeginMainFrameState::READY_TO_COMMIT);
 
   // Expect to commit regardless of BeginImplFrame state.
   EXPECT_IMPL_FRAME_STATE(
-      SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_BEGIN_FRAME);
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_COMMIT);
+      SchedulerStateMachine::BeginImplFrameState::INSIDE_BEGIN_FRAME);
+  EXPECT_ACTION(SchedulerStateMachine::Action::COMMIT);
 
   state.OnBeginImplFrameDeadline();
   EXPECT_IMPL_FRAME_STATE(
-      SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE);
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_COMMIT);
+      SchedulerStateMachine::BeginImplFrameState::INSIDE_DEADLINE);
+  EXPECT_ACTION(SchedulerStateMachine::Action::COMMIT);
 
   state.OnBeginImplFrameIdle();
-  EXPECT_IMPL_FRAME_STATE(SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_IDLE);
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_IMPL_FRAME_STATE(SchedulerStateMachine::BeginImplFrameState::IDLE);
+  EXPECT_ACTION(SchedulerStateMachine::Action::COMMIT);
 
   state.IssueNextBeginImplFrame();
   EXPECT_IMPL_FRAME_STATE(
-      SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_BEGIN_FRAME);
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_COMMIT);
+      SchedulerStateMachine::BeginImplFrameState::INSIDE_BEGIN_FRAME);
+  EXPECT_ACTION(SchedulerStateMachine::Action::COMMIT);
 
   // Finish the commit and activate, then make sure we start the next commit
   // immediately and draw on the next BeginImplFrame.
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   state.OnBeginImplFrameDeadline();
 
   EXPECT_TRUE(state.active_tree_needs_first_draw());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
   state.DidReceiveCompositorFrameAck();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest, TestFullCycle) {
@@ -965,38 +967,38 @@
   // Begin the frame.
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::SENT);
   EXPECT_FALSE(state.NeedsCommit());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Tell the scheduler the frame finished.
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
   EXPECT_MAIN_FRAME_STATE(
-      SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_READY_TO_COMMIT);
+      SchedulerStateMachine::BeginMainFrameState::READY_TO_COMMIT);
 
   // Commit.
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
 
   // Activate.
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
   EXPECT_TRUE(state.active_tree_needs_first_draw());
   EXPECT_TRUE(state.needs_redraw());
 
   // Expect to do nothing until BeginImplFrame deadline
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // At BeginImplFrame deadline, draw.
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
   state.DidReceiveCompositorFrameAck();
 
   // Should be synchronized, no draw needed, no action needed.
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::IDLE);
   EXPECT_FALSE(state.needs_redraw());
 }
 
@@ -1011,12 +1013,12 @@
   // Make a main frame, commit and activate it. But don't draw it.
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
 
   // Try to make a new main frame before drawing. Since we will commit it to a
   // pending tree and not clobber the active tree, we're able to start a new
@@ -1024,10 +1026,10 @@
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
 }
 
 TEST(SchedulerStateMachineTest, DontCommitWithoutDrawWithoutPendingTree) {
@@ -1043,18 +1045,18 @@
   // Make a main frame, commit and activate it. But don't draw it.
   state.OnBeginImplFrame(0, 10);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
 
   // Try to make a new main frame before drawing, but since we would clobber the
   // active tree, we will not do so.
   state.SetNeedsBeginMainFrame();
   state.OnBeginImplFrame(0, 11);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest, AbortedMainFrameDoesNotResetPendingTree) {
@@ -1067,48 +1069,48 @@
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.has_pending_tree());
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Ask for another commit but abort it. Verify that we didn't reset pending
   // tree state.
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.has_pending_tree());
   state.NotifyBeginMainFrameStarted();
   state.BeginMainFrameAborted(CommitEarlyOutReason::FINISHED_NO_UPDATES);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.has_pending_tree());
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Ask for another commit that doesn't abort.
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.has_pending_tree());
 
   // Verify that commit is delayed until the pending tree is activated.
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
   EXPECT_FALSE(state.has_pending_tree());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.has_pending_tree());
 }
 
@@ -1125,68 +1127,68 @@
   // Begin the frame.
   state.OnBeginImplFrame(0, 10);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::SENT);
   EXPECT_FALSE(state.NeedsCommit());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Tell the scheduler the frame finished.
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
   EXPECT_MAIN_FRAME_STATE(
-      SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_READY_TO_COMMIT);
+      SchedulerStateMachine::BeginMainFrameState::READY_TO_COMMIT);
   // Commit.
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   // Commit always calls NotifyReadyToActivate in this mode.
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // No draw because we haven't received NotifyReadyToDraw yet.
   state.OnBeginImplFrameDeadline();
   EXPECT_TRUE(state.active_tree_needs_first_draw());
   EXPECT_TRUE(state.needs_redraw());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Can't BeginMainFrame yet since last commit hasn't been drawn yet.
   state.SetNeedsBeginMainFrame();
   state.OnBeginImplFrame(0, 11);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Now call ready to draw which will allow the draw to happen and
   // BeginMainFrame to be sent.
   state.NotifyReadyToDraw();
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   // Submit throttled from this point.
   state.DidSubmitCompositorFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Can't BeginMainFrame yet since we're submit-frame throttled.
   state.OnBeginImplFrame(0, 12);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // CompositorFrameAck unblocks BeginMainFrame.
   state.DidReceiveCompositorFrameAck();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Draw the newly activated tree.
   state.NotifyReadyToDraw();
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // When commits are deferred, we don't block the deadline.
   state.SetDeferCommits(true);
   state.OnBeginImplFrame(0, 13);
-  EXPECT_NE(SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_BLOCKED,
+  EXPECT_NE(SchedulerStateMachine::BeginImplFrameDeadlineMode::BLOCKED,
             state.CurrentBeginImplFrameDeadlineMode());
 }
 
@@ -1201,46 +1203,46 @@
   // Begin the frame.
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::SENT);
   EXPECT_FALSE(state.NeedsCommit());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Request another commit while the commit is in flight.
   state.SetNeedsBeginMainFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Tell the scheduler the frame finished.
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
   EXPECT_MAIN_FRAME_STATE(
-      SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_READY_TO_COMMIT);
+      SchedulerStateMachine::BeginMainFrameState::READY_TO_COMMIT);
 
   // First commit and activate.
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
   EXPECT_TRUE(state.active_tree_needs_first_draw());
   EXPECT_TRUE(state.needs_redraw());
 
   // Expect to do nothing until BeginImplFrame deadline.
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // At BeginImplFrame deadline, draw.
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
   state.DidReceiveCompositorFrameAck();
 
   // Should be synchronized, no draw needed, no action needed.
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::IDLE);
   EXPECT_FALSE(state.needs_redraw());
 
   // Next BeginImplFrame should initiate second commit.
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
 }
 
 TEST(SchedulerStateMachineTest, TestNoRequestCommitWhenInvisible) {
@@ -1248,13 +1250,13 @@
   StateMachine state(default_scheduler_settings);
   state.SetVisible(true);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
   state.SetVisible(false);
   state.SetNeedsBeginMainFrame();
   EXPECT_FALSE(state.CouldSendBeginMainFrame());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest, TestNoRequestCommitWhenBeginFrameSourcePaused) {
@@ -1262,32 +1264,32 @@
   StateMachine state(default_scheduler_settings);
   state.SetVisible(true);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
   state.SetBeginFrameSourcePaused(true);
   state.SetNeedsBeginMainFrame();
   EXPECT_FALSE(state.CouldSendBeginMainFrame());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest, TestNoRequestLayerTreeFrameSinkWhenInvisible) {
   SchedulerSettings default_scheduler_settings;
   StateMachine state(default_scheduler_settings);
   // We should not request a LayerTreeFrameSink when we are still invisible.
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.SetVisible(true);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
   state.SetVisible(false);
   state.DidLoseLayerTreeFrameSink();
   state.SetNeedsBeginMainFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.SetVisible(true);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
 }
 
 // See ProxyMain::BeginMainFrame "EarlyOut_NotVisible" /
@@ -1303,10 +1305,10 @@
   // Begin the frame while visible.
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::SENT);
   EXPECT_FALSE(state.NeedsCommit());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Become invisible and abort BeginMainFrame.
   state.SetVisible(false);
@@ -1318,30 +1320,30 @@
   EXPECT_TRUE(state.NeedsCommit());
 
   // We should now be back in the idle state as if we never started the frame.
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::IDLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // We shouldn't do anything on the BeginImplFrame deadline.
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Become visible again.
   state.SetVisible(true);
 
   // Although we have aborted on this frame and haven't cancelled the commit
   // (i.e. need another), don't send another BeginMainFrame yet.
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::IDLE);
+  EXPECT_ACTION(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.NeedsCommit());
 
   // Start a new frame.
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
 
   // We should be starting the commit now.
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::SENT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 // See ProxyMain::BeginMainFrame "EarlyOut_NoUpdates" case.
@@ -1350,8 +1352,8 @@
   StateMachine state(default_scheduler_settings);
   state.SetVisible(true);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.DidCreateAndInitializeLayerTreeFrameSink();
   state.SetCanDraw(true);
 
@@ -1359,10 +1361,10 @@
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::SENT);
   EXPECT_FALSE(state.NeedsCommit());
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION(SchedulerStateMachine::Action::NONE);
 
   // Abort the commit, true means that the BeginMainFrame was sent but there
   // was no work to do on the main thread.
@@ -1373,24 +1375,24 @@
   EXPECT_FALSE(state.NeedsCommit());
 
   // Since the commit was aborted, we don't need to try and draw.
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Verify another commit doesn't start on another frame either.
   EXPECT_FALSE(state.NeedsCommit());
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::IDLE);
 
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Verify another commit can start if requested, though.
   state.SetNeedsBeginMainFrame();
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::IDLE);
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION(SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
 }
 
 TEST(SchedulerStateMachineTest, TestFirstContextCreation) {
@@ -1400,21 +1402,21 @@
   state.SetCanDraw(true);
 
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
   state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Check that the first init does not SetNeedsBeginMainFrame.
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Check that a needs commit initiates a BeginMainFrame.
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
 }
 
 TEST(SchedulerStateMachineTest, TestContextLostWhenCompletelyIdle) {
@@ -1422,16 +1424,16 @@
   StateMachine state(default_scheduler_settings);
   SET_UP_STATE(state)
 
-  EXPECT_NE(SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION,
+  EXPECT_NE(SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION,
             state.NextAction());
   state.DidLoseLayerTreeFrameSink();
 
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Once context recreation begins, nothing should happen.
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Recreate the context.
   state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
@@ -1439,7 +1441,7 @@
   // When the context is recreated, we should begin a commit.
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -1448,92 +1450,92 @@
   StateMachine state(default_scheduler_settings);
   SET_UP_STATE(state)
 
-  EXPECT_NE(SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION,
+  EXPECT_NE(SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION,
             state.NextAction());
   state.DidLoseLayerTreeFrameSink();
   EXPECT_EQ(state.layer_tree_frame_sink_state(),
-            SchedulerStateMachine::LAYER_TREE_FRAME_SINK_NONE);
+            SchedulerStateMachine::LayerTreeFrameSinkState::NONE);
 
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Once context recreation begins, nothing should happen.
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // While context is recreating, commits shouldn't begin.
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Recreate the context
   state.DidCreateAndInitializeLayerTreeFrameSink();
   EXPECT_EQ(
       state.layer_tree_frame_sink_state(),
-      SchedulerStateMachine::LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_COMMIT);
+      SchedulerStateMachine::LayerTreeFrameSinkState::WAITING_FOR_FIRST_COMMIT);
   EXPECT_FALSE(state.RedrawPending());
 
   // When the context is recreated, we wait until the next BeginImplFrame
   // before starting.
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // When the BeginFrame comes in we should begin a commit
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::SENT);
 
   // Until that commit finishes, we shouldn't be drawing.
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Finish the commit, which should make the surface active.
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   EXPECT_EQ(state.layer_tree_frame_sink_state(),
-            SchedulerStateMachine::
-                LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_ACTIVATION);
+            SchedulerStateMachine::LayerTreeFrameSinkState::
+                WAITING_FOR_FIRST_ACTIVATION);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_EQ(state.layer_tree_frame_sink_state(),
-            SchedulerStateMachine::LAYER_TREE_FRAME_SINK_ACTIVE);
+            SchedulerStateMachine::LayerTreeFrameSinkState::ACTIVE);
 
   // Finishing the first commit after initializing a LayerTreeFrameSink should
   // automatically cause a redraw.
   EXPECT_TRUE(state.RedrawPending());
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_FALSE(state.RedrawPending());
 
   // Next frame as no work to do.
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Once the context is recreated, whether we draw should be based on
   // SetCanDraw if waiting on first draw after activate.
   state.SetNeedsRedraw(true);
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.SetCanDraw(false);
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION(SchedulerStateMachine::Action::NONE);
   state.SetCanDraw(true);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Once the context is recreated, whether we draw should be based on
   // SetCanDraw if waiting on first draw after activate.
@@ -1541,24 +1543,24 @@
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   // Activate so we need the first draw
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.active_tree_needs_first_draw());
   EXPECT_TRUE(state.needs_redraw());
 
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.SetCanDraw(false);
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_DRAW_ABORT);
+  EXPECT_ACTION(SchedulerStateMachine::Action::DRAW_ABORT);
   state.SetCanDraw(true);
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
 }
 
 TEST(SchedulerStateMachineTest, TestContextLostWhileCommitInProgress) {
@@ -1573,13 +1575,13 @@
   state.SetNeedsRedraw(true);
   state.OnBeginImplFrame(0, 10);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
   state.DidReceiveCompositorFrameAck();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Cause a lost context while the BeginMainFrame is in flight.
   state.DidLoseLayerTreeFrameSink();
@@ -1587,34 +1589,34 @@
 
   // Ask for another draw. Expect nothing happens.
   state.SetNeedsRedraw(true);
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION(SchedulerStateMachine::Action::NONE);
 
   // Finish the frame, commit and activate.
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
 
   // We will abort the draw when the LayerTreeFrameSink is lost if we are
   // waiting for the first draw to unblock the main thread.
   EXPECT_TRUE(state.active_tree_needs_first_draw());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_ABORT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_ABORT);
 
-  // Expect to begin context recreation only in BEGIN_IMPL_FRAME_STATE_IDLE
-  EXPECT_IMPL_FRAME_STATE(SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_IDLE);
+  // Expect to begin context recreation only in BeginImplFrameState::IDLE
+  EXPECT_IMPL_FRAME_STATE(SchedulerStateMachine::BeginImplFrameState::IDLE);
   EXPECT_ACTION(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
 
   state.OnBeginImplFrame(0, 11);
   EXPECT_IMPL_FRAME_STATE(
-      SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_BEGIN_FRAME);
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::BeginImplFrameState::INSIDE_BEGIN_FRAME);
+  EXPECT_ACTION(SchedulerStateMachine::Action::NONE);
 
   state.OnBeginImplFrameDeadline();
   EXPECT_IMPL_FRAME_STATE(
-      SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE);
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::BeginImplFrameState::INSIDE_DEADLINE);
+  EXPECT_ACTION(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -1625,19 +1627,19 @@
 
   // Get a commit in flight.
   state.SetNeedsBeginMainFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Set damage and expect a draw.
   state.SetNeedsRedraw(true);
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
   state.DidReceiveCompositorFrameAck();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Cause a lost context while the BeginMainFrame is in flight.
   state.DidLoseLayerTreeFrameSink();
@@ -1645,56 +1647,56 @@
   // Ask for another draw and also set needs commit. Expect nothing happens.
   state.SetNeedsRedraw(true);
   state.SetNeedsBeginMainFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Finish the frame, and commit and activate.
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
   EXPECT_TRUE(state.active_tree_needs_first_draw());
 
   // Because the LayerTreeFrameSink is missing, we expect the draw to abort.
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_ABORT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_ABORT);
 
-  // Expect to begin context recreation only in BEGIN_IMPL_FRAME_STATE_IDLE
-  EXPECT_IMPL_FRAME_STATE(SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_IDLE);
+  // Expect to begin context recreation only in BeginImplFrameState::IDLE
+  EXPECT_IMPL_FRAME_STATE(SchedulerStateMachine::BeginImplFrameState::IDLE);
   EXPECT_ACTION(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
 
   state.IssueNextBeginImplFrame();
   EXPECT_IMPL_FRAME_STATE(
-      SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_BEGIN_FRAME);
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::BeginImplFrameState::INSIDE_BEGIN_FRAME);
+  EXPECT_ACTION(SchedulerStateMachine::Action::NONE);
 
   state.OnBeginImplFrameDeadline();
   EXPECT_IMPL_FRAME_STATE(
-      SchedulerStateMachine::BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE);
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::BeginImplFrameState::INSIDE_DEADLINE);
+  EXPECT_ACTION(SchedulerStateMachine::Action::NONE);
 
   state.OnBeginImplFrameIdle();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
 
   // After we get a new LayerTreeFrameSink, the commit flow should start.
   state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
   state.DidReceiveCompositorFrameAck();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -1708,13 +1710,13 @@
   // Cause a lost LayerTreeFrameSink, and restore it.
   state.DidLoseLayerTreeFrameSink();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.DidCreateAndInitializeLayerTreeFrameSink();
 
   EXPECT_FALSE(state.RedrawPending());
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION(SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -1724,20 +1726,20 @@
   SET_UP_STATE(state)
 
   state.SetBeginMainFrameState(
-      SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT);
+      SchedulerStateMachine::BeginMainFrameState::SENT);
 
   // Cause a lost context.
   state.DidLoseLayerTreeFrameSink();
 
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
 
   EXPECT_TRUE(state.ShouldAbortCurrentFrame());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
 
   EXPECT_TRUE(state.PendingDrawsShouldBeAborted());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_ABORT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_ABORT);
 }
 
 TEST(SchedulerStateMachineTest, TestNoBeginFrameNeededWhenInvisible) {
@@ -1745,8 +1747,8 @@
   StateMachine state(default_scheduler_settings);
   state.SetVisible(true);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
 
   EXPECT_FALSE(state.BeginFrameNeeded());
@@ -1765,12 +1767,12 @@
   StateMachine state(default_scheduler_settings);
   state.SetVisible(true);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
   state.SetVisible(false);
   state.SetNeedsBeginMainFrame();
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION(SchedulerStateMachine::Action::NONE);
   EXPECT_FALSE(state.BeginFrameNeeded());
 
   // When become visible again, the needs commit should still be pending.
@@ -1778,7 +1780,7 @@
   EXPECT_TRUE(state.BeginFrameNeeded());
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
 }
 
 TEST(SchedulerStateMachineTest, TestFinishCommitWhenCommitInProgress) {
@@ -1786,23 +1788,23 @@
   StateMachine state(default_scheduler_settings);
   state.SetVisible(true);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
   state.SetVisible(false);
   state.SetBeginMainFrameState(
-      SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT);
+      SchedulerStateMachine::BeginMainFrameState::SENT);
   state.SetNeedsBeginMainFrame();
 
   // After the commit completes, activation and draw happen immediately
   // because we are not visible.
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
   EXPECT_TRUE(state.active_tree_needs_first_draw());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_ABORT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_ABORT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -1811,12 +1813,12 @@
   StateMachine state(default_scheduler_settings);
   state.SetVisible(true);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.CreateAndInitializeLayerTreeFrameSinkWithActivatedCommit();
   state.SetBeginFrameSourcePaused(true);
   state.SetBeginMainFrameState(
-      SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT);
+      SchedulerStateMachine::BeginMainFrameState::SENT);
   state.SetNeedsBeginMainFrame();
 
   // After the commit completes, activation and draw happen immediately
@@ -1824,11 +1826,11 @@
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
   EXPECT_TRUE(state.ShouldAbortCurrentFrame());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
   EXPECT_TRUE(state.active_tree_needs_first_draw());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_ABORT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_ABORT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest, TestInitialActionsWhenContextLost) {
@@ -1841,19 +1843,19 @@
   // When we are visible, we normally want to begin LayerTreeFrameSink creation
   // as soon as possible.
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
 
   state.DidCreateAndInitializeLayerTreeFrameSink();
   EXPECT_EQ(
       state.layer_tree_frame_sink_state(),
-      SchedulerStateMachine::LAYER_TREE_FRAME_SINK_WAITING_FOR_FIRST_COMMIT);
+      SchedulerStateMachine::LayerTreeFrameSinkState::WAITING_FOR_FIRST_COMMIT);
 
   // We should not send a BeginMainFrame when we are invisible, even if we've
   // lost the LayerTreeFrameSink and are trying to get the first commit, since
   // the
   // main thread will just abort anyway.
   state.SetVisible(false);
-  EXPECT_ACTION(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest, ReportIfNotDrawing) {
@@ -1949,8 +1951,8 @@
 
   // We should start the commit normally.
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Since only the scroll offset changed, the main thread will abort the
   // commit.
@@ -1969,18 +1971,18 @@
 
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   EXPECT_TRUE(state.ShouldTriggerBeginImplFrameDeadlineImmediately());
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
 }
 
@@ -1995,8 +1997,8 @@
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Verify the deadline is not triggered early until we enter
   // prefer impl latency mode.
@@ -2008,33 +2010,33 @@
 
   // Trigger the deadline.
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.DidReceiveCompositorFrameAck();
 
   // Request a new commit and finish the previous one.
   state.SetNeedsBeginMainFrame();
   FinishPreviousCommitAndDrawWithoutExitingDeadline(&state);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.DidReceiveCompositorFrameAck();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Finish the previous commit and draw it.
   FinishPreviousCommitAndDrawWithoutExitingDeadline(&state);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Verify we do not send another BeginMainFrame if was are submit-frame
   // throttled and did not just submit one.
   state.SetNeedsBeginMainFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_FALSE(state.ShouldTriggerBeginImplFrameDeadlineImmediately());
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -2047,12 +2049,12 @@
 
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_FALSE(state.ShouldTriggerBeginImplFrameDeadlineImmediately());
 
   state.DidLoseLayerTreeFrameSink();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   // The deadline should be triggered immediately when LayerTreeFrameSink is
   // lost.
   EXPECT_TRUE(state.ShouldTriggerBeginImplFrameDeadlineImmediately());
@@ -2067,12 +2069,12 @@
 
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_FALSE(state.ShouldTriggerBeginImplFrameDeadlineImmediately());
 
   state.SetVisible(false);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.ShouldAbortCurrentFrame());
   EXPECT_TRUE(state.ShouldTriggerBeginImplFrameDeadlineImmediately());
 }
@@ -2087,12 +2089,12 @@
 
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_FALSE(state.ShouldTriggerBeginImplFrameDeadlineImmediately());
 
   state.SetBeginFrameSourcePaused(true);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   EXPECT_TRUE(state.ShouldAbortCurrentFrame());
   EXPECT_TRUE(state.ShouldTriggerBeginImplFrameDeadlineImmediately());
 }
@@ -2106,18 +2108,18 @@
 
   state.SetNeedsBeginMainFrame();
   EXPECT_FALSE(state.BeginFrameNeeded());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   state.SetDeferCommits(false);
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
 }
 
 TEST(SchedulerStateMachineTest, EarlyOutCommitWantsProactiveBeginFrame) {
@@ -2142,20 +2144,20 @@
   // Set up the request for a commit and start a frame.
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
-  PerformAction(&state, SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+  PerformAction(&state, SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
 
   // Lose the LayerTreeFrameSink.
   state.DidLoseLayerTreeFrameSink();
 
   // The scheduler shouldn't trigger the LayerTreeFrameSink creation till the
   // previous commit has been cleared.
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Trigger the deadline and ensure that the scheduler does not trigger any
   // actions until we receive a response for the pending commit.
   state.OnBeginImplFrameDeadline();
   state.OnBeginImplFrameIdle();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Abort the commit, since that is what we expect the main thread to do if the
   // LayerTreeFrameSink was lost due to a synchronous call from the main thread
@@ -2166,7 +2168,7 @@
 
   // The scheduler should begin the LayerTreeFrameSink creation now.
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
 }
 
 TEST(SchedulerStateMachineTest, NoImplSideInvalidationsWhileInvisible) {
@@ -2180,7 +2182,7 @@
   state.SetNeedsImplSideInvalidation(needs_first_draw_on_activation);
   state.IssueNextBeginImplFrame();
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -2196,7 +2198,7 @@
   state.SetNeedsImplSideInvalidation(needs_first_draw_on_activation);
   state.IssueNextBeginImplFrame();
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -2211,7 +2213,7 @@
   state.SetNeedsImplSideInvalidation(needs_first_draw_on_activation);
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_PERFORM_IMPL_SIDE_INVALIDATION);
+      SchedulerStateMachine::Action::PERFORM_IMPL_SIDE_INVALIDATION);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -2227,10 +2229,10 @@
   state.SetNeedsImplSideInvalidation(needs_first_draw_on_activation);
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
   state.OnBeginImplFrameDeadline();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_PERFORM_IMPL_SIDE_INVALIDATION);
+      SchedulerStateMachine::Action::PERFORM_IMPL_SIDE_INVALIDATION);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -2244,23 +2246,23 @@
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   bool needs_first_draw_on_activation = true;
   state.SetNeedsImplSideInvalidation(needs_first_draw_on_activation);
   state.IssueNextBeginImplFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_PERFORM_IMPL_SIDE_INVALIDATION);
+      SchedulerStateMachine::Action::PERFORM_IMPL_SIDE_INVALIDATION);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -2274,19 +2276,19 @@
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
   state.NotifyBeginMainFrameStarted();
   state.BeginMainFrameAborted(CommitEarlyOutReason::FINISHED_NO_UPDATES);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   bool needs_first_draw_on_activation = true;
   state.SetNeedsImplSideInvalidation(needs_first_draw_on_activation);
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_PERFORM_IMPL_SIDE_INVALIDATION);
+      SchedulerStateMachine::Action::PERFORM_IMPL_SIDE_INVALIDATION);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -2299,27 +2301,27 @@
   // initialized.
   state.DidLoseLayerTreeFrameSink();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::BEGIN_LAYER_TREE_FRAME_SINK_CREATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // No impl-side invalidations should be performed during frame sink creation.
   bool needs_first_draw_on_activation = true;
   state.SetNeedsImplSideInvalidation(needs_first_draw_on_activation);
   state.IssueNextBeginImplFrame();
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Initializing the LayerTreeFrameSink puts us in a state waiting for the
   // first commit.
   state.DidCreateAndInitializeLayerTreeFrameSink();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
   state.NotifyBeginMainFrameStarted();
   state.BeginMainFrameAborted(CommitEarlyOutReason::FINISHED_NO_UPDATES);
   state.OnBeginImplFrameDeadline();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_PERFORM_IMPL_SIDE_INVALIDATION);
+      SchedulerStateMachine::Action::PERFORM_IMPL_SIDE_INVALIDATION);
 }
 
 TEST(SchedulerStateMachineTest, ImplSideInvalidationWhenPendingTreeExists) {
@@ -2331,32 +2333,32 @@
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
 
   // Request an impl-side invalidation after the commit. The request should wait
   // till the current pending tree is activated.
   bool needs_first_draw_on_activation = true;
   state.SetNeedsImplSideInvalidation(needs_first_draw_on_activation);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Activate the pending tree. Since the commit fills the impl-side
   // invalidation funnel as well, the request should wait until the next
   // BeginFrame.
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Since there is no main frame request, this should perform impl-side
   // invalidations.
   state.IssueNextBeginImplFrame();
   state.OnBeginImplFrameDeadline();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_PERFORM_IMPL_SIDE_INVALIDATION);
+      SchedulerStateMachine::Action::PERFORM_IMPL_SIDE_INVALIDATION);
 }
 
 TEST(SchedulerStateMachineTest, ImplSideInvalidationWhileReadyToCommit) {
@@ -2369,26 +2371,26 @@
   state.IssueNextBeginImplFrame();
 
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Request an impl-side invalidation. The request should wait till a response
   // is received from the main thread.
   bool needs_first_draw_on_activation = true;
   state.SetNeedsImplSideInvalidation(needs_first_draw_on_activation);
   EXPECT_TRUE(state.needs_impl_side_invalidation());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Perform a commit, the impl-side invalidation request should be reset since
   // they will be merged with the commit.
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   EXPECT_FALSE(state.needs_impl_side_invalidation());
 
   // Deadline.
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -2403,24 +2405,24 @@
   state.IssueNextBeginImplFrame();
   state.OnBeginImplFrameDeadline();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_PERFORM_IMPL_SIDE_INVALIDATION);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::PERFORM_IMPL_SIDE_INVALIDATION);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Request another invalidation, which should wait until the pending tree is
   // activated *and* we start the next BeginFrame.
   state.SetNeedsImplSideInvalidation(needs_first_draw_on_activation);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Now start the next frame, which will first draw the active tree and then
   // perform the pending impl-side invalidation request.
   state.IssueNextBeginImplFrame();
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_PERFORM_IMPL_SIDE_INVALIDATION);
+      SchedulerStateMachine::Action::PERFORM_IMPL_SIDE_INVALIDATION);
 }
 
 TEST(SchedulerStateMachineTest, ImplSideInvalidationsThrottledOnDraw) {
@@ -2435,17 +2437,17 @@
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
   state.NotifyReadyToDraw();
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Request impl-side invalidation and start a new frame, which should be
   // blocked on the ack for the previous frame.
@@ -2453,7 +2455,7 @@
   state.SetNeedsImplSideInvalidation(needs_first_draw_on_activation);
   state.IssueNextBeginImplFrame();
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Ack the previous frame and begin impl frame, which should perform the
   // invalidation now.
@@ -2461,7 +2463,7 @@
   state.IssueNextBeginImplFrame();
   state.OnBeginImplFrameDeadline();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_PERFORM_IMPL_SIDE_INVALIDATION);
+      SchedulerStateMachine::Action::PERFORM_IMPL_SIDE_INVALIDATION);
 }
 
 TEST(SchedulerStateMachineTest,
@@ -2474,8 +2476,8 @@
   state.SetNeedsBeginMainFrame();
   state.IssueNextBeginImplFrame();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Request an impl-side invalidation and trigger the deadline, the
   // invalidation should run if the request is still pending when we enter the
@@ -2484,7 +2486,7 @@
   state.SetNeedsImplSideInvalidation(needs_first_draw_on_activation);
   state.OnBeginImplFrameDeadline();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_PERFORM_IMPL_SIDE_INVALIDATION);
+      SchedulerStateMachine::Action::PERFORM_IMPL_SIDE_INVALIDATION);
 }
 
 TEST(SchedulerStateMachineTest, PrepareTilesWaitForImplSideInvalidation) {
@@ -2501,9 +2503,9 @@
   state.IssueNextBeginImplFrame();
   state.OnBeginImplFrameDeadline();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_PERFORM_IMPL_SIDE_INVALIDATION);
+      SchedulerStateMachine::Action::PERFORM_IMPL_SIDE_INVALIDATION);
   state.DidPrepareTiles();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 }
 
 TEST(SchedulerStateMachineTest, TestFullPipelineMode) {
@@ -2522,73 +2524,73 @@
   // Begin the frame.
   state.OnBeginImplFrame(0, 10);
   // We are blocking because we need a main frame.
-  EXPECT_EQ(SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_BLOCKED,
+  EXPECT_EQ(SchedulerStateMachine::BeginImplFrameDeadlineMode::BLOCKED,
             state.CurrentBeginImplFrameDeadlineMode());
 
   // Even if main thread defers commits, we still need to wait for it.
   state.SetDeferCommits(true);
-  EXPECT_EQ(SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_BLOCKED,
+  EXPECT_EQ(SchedulerStateMachine::BeginImplFrameDeadlineMode::BLOCKED,
             state.CurrentBeginImplFrameDeadlineMode());
   state.SetDeferCommits(false);
 
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_SENT);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::SENT);
   EXPECT_FALSE(state.NeedsCommit());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   // We are blocking on the main frame.
-  EXPECT_EQ(SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_BLOCKED,
+  EXPECT_EQ(SchedulerStateMachine::BeginImplFrameDeadlineMode::BLOCKED,
             state.CurrentBeginImplFrameDeadlineMode());
 
   // Tell the scheduler the frame finished.
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
   EXPECT_MAIN_FRAME_STATE(
-      SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_READY_TO_COMMIT);
+      SchedulerStateMachine::BeginMainFrameState::READY_TO_COMMIT);
   // We are blocking on commit.
-  EXPECT_EQ(SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_BLOCKED,
+  EXPECT_EQ(SchedulerStateMachine::BeginImplFrameDeadlineMode::BLOCKED,
             state.CurrentBeginImplFrameDeadlineMode());
   // Commit.
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
   // We are blocking on activation.
-  EXPECT_EQ(SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_BLOCKED,
+  EXPECT_EQ(SchedulerStateMachine::BeginImplFrameDeadlineMode::BLOCKED,
             state.CurrentBeginImplFrameDeadlineMode());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // We should prepare tiles even though we are not in the deadline, otherwise
   // we would get stuck here.
   EXPECT_FALSE(state.ShouldPrepareTiles());
   state.SetNeedsPrepareTiles();
   EXPECT_TRUE(state.ShouldPrepareTiles());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_PREPARE_TILES);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::PREPARE_TILES);
 
   // Ready to activate, but not draw.
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
   // We should no longer block, because can_draw is still false, and we are no
   // longer waiting for activation.
-  EXPECT_EQ(SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_IMMEDIATE,
+  EXPECT_EQ(SchedulerStateMachine::BeginImplFrameDeadlineMode::IMMEDIATE,
             state.CurrentBeginImplFrameDeadlineMode());
 
   // However, we should continue to block on ready to draw if we can draw.
   state.SetCanDraw(true);
-  EXPECT_EQ(SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_BLOCKED,
+  EXPECT_EQ(SchedulerStateMachine::BeginImplFrameDeadlineMode::BLOCKED,
             state.CurrentBeginImplFrameDeadlineMode());
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Ready to draw triggers immediate deadline.
   state.NotifyReadyToDraw();
-  EXPECT_EQ(SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_IMMEDIATE,
+  EXPECT_EQ(SchedulerStateMachine::BeginImplFrameDeadlineMode::IMMEDIATE,
             state.CurrentBeginImplFrameDeadlineMode());
 
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
   state.DidSubmitCompositorFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   // In full-pipe mode, CompositorFrameAck should always arrive before any
   // subsequent BeginFrame.
   state.DidReceiveCompositorFrameAck();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Request a redraw without main frame.
   state.SetNeedsRedraw(true);
@@ -2596,20 +2598,20 @@
   // Redraw should happen immediately since there is no pending tree and active
   // tree is ready to draw.
   state.OnBeginImplFrame(0, 11);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
-  EXPECT_EQ(SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_IMMEDIATE,
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
+  EXPECT_EQ(SchedulerStateMachine::BeginImplFrameDeadlineMode::IMMEDIATE,
             state.CurrentBeginImplFrameDeadlineMode());
 
   // Redraw on impl-side only.
   state.OnBeginImplFrameDeadline();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_DRAW_IF_POSSIBLE);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::DRAW_IF_POSSIBLE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   state.DidSubmitCompositorFrame();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   // In full-pipe mode, CompositorFrameAck should always arrive before any
   // subsequent BeginFrame.
   state.DidReceiveCompositorFrameAck();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
 
   // Request a redraw on active frame and a main frame.
   state.SetNeedsRedraw(true);
@@ -2617,10 +2619,10 @@
 
   state.OnBeginImplFrame(0, 12);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
   // Blocked on main frame.
-  EXPECT_EQ(SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_BLOCKED,
+  EXPECT_EQ(SchedulerStateMachine::BeginImplFrameDeadlineMode::BLOCKED,
             state.CurrentBeginImplFrameDeadlineMode());
 
   // Even with SMOOTHNESS_TAKES_PRIORITY, we don't prioritize impl thread and we
@@ -2628,15 +2630,15 @@
   state.SetTreePrioritiesAndScrollState(
       SMOOTHNESS_TAKES_PRIORITY,
       ScrollHandlerState::SCROLL_DOES_NOT_AFFECT_SCROLL_HANDLER);
-  EXPECT_EQ(SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_BLOCKED,
+  EXPECT_EQ(SchedulerStateMachine::BeginImplFrameDeadlineMode::BLOCKED,
             state.CurrentBeginImplFrameDeadlineMode());
 
   // Abort commit and ensure that we don't block anymore.
   state.NotifyBeginMainFrameStarted();
   state.BeginMainFrameAborted(CommitEarlyOutReason::FINISHED_NO_UPDATES);
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_NONE);
-  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BEGIN_MAIN_FRAME_STATE_IDLE);
-  EXPECT_EQ(SchedulerStateMachine::BEGIN_IMPL_FRAME_DEADLINE_MODE_IMMEDIATE,
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::NONE);
+  EXPECT_MAIN_FRAME_STATE(SchedulerStateMachine::BeginMainFrameState::IDLE);
+  EXPECT_EQ(SchedulerStateMachine::BeginImplFrameDeadlineMode::IMMEDIATE,
             state.CurrentBeginImplFrameDeadlineMode());
 }
 
@@ -2652,23 +2654,23 @@
   state.OnBeginImplFrame(0, 1);
   state.OnBeginImplFrameDeadline();
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_PERFORM_IMPL_SIDE_INVALIDATION);
+      SchedulerStateMachine::Action::PERFORM_IMPL_SIDE_INVALIDATION);
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
   state.OnBeginImplFrameIdle();
 
   // Now we have a main frame.
   state.SetNeedsBeginMainFrame();
   state.OnBeginImplFrame(0, 2);
   EXPECT_ACTION_UPDATE_STATE(
-      SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME);
+      SchedulerStateMachine::Action::SEND_BEGIN_MAIN_FRAME);
   state.NotifyBeginMainFrameStarted();
   state.NotifyReadyToCommit();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_COMMIT);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::COMMIT);
 
   // We should be able to activate this tree without drawing the active tree.
   state.NotifyReadyToActivate();
-  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::ACTION_ACTIVATE_SYNC_TREE);
+  EXPECT_ACTION_UPDATE_STATE(SchedulerStateMachine::Action::ACTIVATE_SYNC_TREE);
 }
 
 }  // namespace
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/download/ui/DownloadItemView.java b/chrome/android/java/src/org/chromium/chrome/browser/download/ui/DownloadItemView.java
index ca2f24a..fcf7671 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/download/ui/DownloadItemView.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/download/ui/DownloadItemView.java
@@ -56,7 +56,7 @@
     // Controls for completed downloads.
     private View mLayoutCompleted;
     private TextView mFilenameCompletedView;
-    private TextView mDescriptionView;
+    private TextView mDescriptionCompletedView;
 
     // Controls for in-progress downloads.
     private View mLayoutInProgress;
@@ -103,7 +103,7 @@
         mLayoutInProgress = findViewById(R.id.progress_layout);
 
         mFilenameCompletedView = (TextView) findViewById(R.id.filename_completed_view);
-        mDescriptionView = (TextView) findViewById(R.id.description_view);
+        mDescriptionCompletedView = (TextView) findViewById(R.id.description_view);
 
         mFilenameInProgressView = (TextView) findViewById(R.id.filename_progress_view);
         mDownloadStatusView = (TextView) findViewById(R.id.status_view);
@@ -189,13 +189,13 @@
 
         if (mThumbnailBitmap == null) updateIconView();
 
-        Context context = mDescriptionView.getContext();
+        Context context = mDescriptionCompletedView.getContext();
         mFilenameCompletedView.setText(item.getDisplayFileName());
         mFilenameInProgressView.setText(item.getDisplayFileName());
 
         String description = String.format(Locale.getDefault(), "%s - %s",
                 Formatter.formatFileSize(context, item.getFileSize()), item.getDisplayHostname());
-        mDescriptionView.setText(description);
+        mDescriptionCompletedView.setText(description);
 
         if (item.isComplete()) {
             showLayout(mLayoutCompleted);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/download/ui/OfflineGroupHeaderView.java b/chrome/android/java/src/org/chromium/chrome/browser/download/ui/OfflineGroupHeaderView.java
index df74132..7721d0d59 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/download/ui/OfflineGroupHeaderView.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/download/ui/OfflineGroupHeaderView.java
@@ -41,9 +41,9 @@
     private DownloadHistoryAdapter mAdapter;
     private DownloadItemSelectionDelegate mSelectionDelegate;
 
-    private TextView mDescriptionView;
+    private TextView mDescriptionTextView;
     private ImageView mExpandImage;
-    private TintedImageView mIconView;
+    private TintedImageView mIconImageView;
 
     public OfflineGroupHeaderView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -65,8 +65,8 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
 
-        mIconView = (TintedImageView) findViewById(R.id.icon_view);
-        mDescriptionView = (TextView) findViewById(R.id.description);
+        mIconImageView = (TintedImageView) findViewById(R.id.icon_view);
+        mDescriptionTextView = (TextView) findViewById(R.id.description);
         mExpandImage = (ImageView) findViewById(R.id.expand_icon);
     }
 
@@ -102,7 +102,7 @@
         String description = String.format(Locale.getDefault(), "%s - %s",
                 Formatter.formatFileSize(getContext(), header.getTotalFileSize()),
                 getContext().getString(R.string.download_manager_offline_header_description));
-        mDescriptionView.setText(description);
+        mDescriptionTextView.setText(description);
         updateExpandIcon(header.isExpanded());
         setChecked(mSelectionDelegate.isHeaderSelected(header));
     }
@@ -117,26 +117,26 @@
     private void updateCheckIcon(boolean checked) {
         if (checked) {
             if (FeatureUtilities.isChromeHomeEnabled()) {
-                mIconView.setBackgroundResource(mIconBackgroundResId);
-                mIconView.getBackground().setLevel(
+                mIconImageView.setBackgroundResource(mIconBackgroundResId);
+                mIconImageView.getBackground().setLevel(
                         getResources().getInteger(R.integer.list_item_level_selected));
             } else {
-                mIconView.setBackgroundColor(mIconBackgroundColorSelected);
+                mIconImageView.setBackgroundColor(mIconBackgroundColorSelected);
             }
 
-            mIconView.setImageResource(R.drawable.ic_check_googblue_24dp);
-            mIconView.setTint(mCheckedIconForegroundColorList);
+            mIconImageView.setImageResource(R.drawable.ic_check_googblue_24dp);
+            mIconImageView.setTint(mCheckedIconForegroundColorList);
         } else {
             if (FeatureUtilities.isChromeHomeEnabled()) {
-                mIconView.setBackgroundResource(mIconBackgroundResId);
-                mIconView.getBackground().setLevel(
+                mIconImageView.setBackgroundResource(mIconBackgroundResId);
+                mIconImageView.getBackground().setLevel(
                         getResources().getInteger(R.integer.list_item_level_default));
             } else {
-                mIconView.setBackgroundColor(mIconBackgroundColor);
+                mIconImageView.setBackgroundColor(mIconBackgroundColor);
             }
 
-            mIconView.setImageResource(R.drawable.ic_chrome);
-            mIconView.setTint(mIconForegroundColorList);
+            mIconImageView.setImageResource(R.drawable.ic_chrome);
+            mIconImageView.setTint(mIconForegroundColorList);
         }
     }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/widget/OverviewListLayout.java b/chrome/android/java/src/org/chromium/chrome/browser/widget/OverviewListLayout.java
index f1cc4b1..a23e574 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/widget/OverviewListLayout.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/widget/OverviewListLayout.java
@@ -34,7 +34,7 @@
  */
 public class OverviewListLayout extends Layout implements AccessibilityTabModelAdapterListener {
     private AccessibilityTabModelWrapper mTabModelWrapper;
-    private final float mDpToPx;
+    private final float mDensity;
     private final BlackHoleEventFilter mBlackHoleEventFilter;
     private final SceneLayer mSceneLayer;
 
@@ -42,7 +42,7 @@
             Context context, LayoutUpdateHost updateHost, LayoutRenderHost renderHost) {
         super(context, updateHost, renderHost);
         mBlackHoleEventFilter = new BlackHoleEventFilter(context);
-        mDpToPx = context.getResources().getDisplayMetrics().density;
+        mDensity = context.getResources().getDisplayMetrics().density;
         mSceneLayer = new SceneLayer();
     }
 
@@ -81,7 +81,7 @@
                 (FrameLayout.LayoutParams) mTabModelWrapper.getLayoutParams();
         if (params == null) return;
 
-        int margin = (int) ((getHeight() - getHeightMinusBrowserControls()) * mDpToPx);
+        int margin = (int) ((getHeight() - getHeightMinusBrowserControls()) * mDensity);
         if (FeatureUtilities.isChromeHomeEnabled()) {
             params.bottomMargin = margin;
         } else {
diff --git a/chrome/app/chromeos_strings.grdp b/chrome/app/chromeos_strings.grdp
index 1fee6fc..685ffde 100644
--- a/chrome/app/chromeos_strings.grdp
+++ b/chrome/app/chromeos_strings.grdp
@@ -3711,6 +3711,11 @@
     Don't remind me again
   </message>
 
+  <!-- Obsolete versions Notification strings-->
+  <message name="IDS_UPDATE_REQUIRED_LOGIN_SCREEN_MESSAGE" desc="The message on login screen to inform the user that policy prevents user sign in before OS version is is updated.">
+    Your device is no longer compliant with the minimum client version specified by your admin. Please update to login.
+  </message>
+
   <!-- Genius App -->
   <message name="IDS_GENIUS_APP_NAME" desc="Name of the genius app in the app shelf">
     Get Help
diff --git a/chrome/browser/android/chrome_feature_list.cc b/chrome/browser/android/chrome_feature_list.cc
index 8ef7b24e..db61e06b 100644
--- a/chrome/browser/android/chrome_feature_list.cc
+++ b/chrome/browser/android/chrome_feature_list.cc
@@ -170,7 +170,7 @@
                                          base::FEATURE_DISABLED_BY_DEFAULT};
 
 const base::Feature kCCTBackgroundTab{"CCTBackgroundTab",
-                                      base::FEATURE_DISABLED_BY_DEFAULT};
+                                      base::FEATURE_ENABLED_BY_DEFAULT};
 
 const base::Feature kCCTExternalLinkHandling{"CCTExternalLinkHandling",
                                              base::FEATURE_ENABLED_BY_DEFAULT};
diff --git a/chrome/browser/android/omnibox/autocomplete_controller_android.cc b/chrome/browser/android/omnibox/autocomplete_controller_android.cc
index d7fc13e..3ad1b302 100644
--- a/chrome/browser/android/omnibox/autocomplete_controller_android.cc
+++ b/chrome/browser/android/omnibox/autocomplete_controller_android.cc
@@ -10,7 +10,6 @@
 #include "base/android/jni_array.h"
 #include "base/android/jni_string.h"
 #include "base/feature_list.h"
-#include "base/memory/ptr_util.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/strings/string16.h"
 #include "base/strings/string_util.h"
@@ -91,7 +90,7 @@
 
 ZeroSuggestPrefetcher::ZeroSuggestPrefetcher(Profile* profile)
     : controller_(new AutocompleteController(
-          base::MakeUnique<ChromeAutocompleteProviderClient>(profile),
+          std::make_unique<ChromeAutocompleteProviderClient>(profile),
           this,
           AutocompleteProvider::TYPE_ZERO_SUGGEST)) {
   // Creating an arbitrary fake_request_source to avoid passing in an invalid
@@ -128,7 +127,7 @@
 
 AutocompleteControllerAndroid::AutocompleteControllerAndroid(Profile* profile)
     : autocomplete_controller_(new AutocompleteController(
-          base::WrapUnique(new ChromeAutocompleteProviderClient(profile)),
+          std::make_unique<ChromeAutocompleteProviderClient>(profile),
           this,
           AutocompleteClassifier::DefaultOmniboxProviders())),
       inside_synchronous_start_(false),
diff --git a/chrome/browser/chrome_browser_main_linux.cc b/chrome/browser/chrome_browser_main_linux.cc
index f41c5a6..0b6ca73 100644
--- a/chrome/browser/chrome_browser_main_linux.cc
+++ b/chrome/browser/chrome_browser_main_linux.cc
@@ -26,6 +26,7 @@
 #if !defined(OS_CHROMEOS)
 #include "base/command_line.h"
 #include "base/linux_util.h"
+#include "chrome/browser/dbus/dbus_thread_linux.h"
 #include "chrome/common/chrome_paths_internal.h"
 #include "chrome/common/chrome_switches.h"
 #include "components/os_crypt/key_storage_config_linux.h"
@@ -70,9 +71,10 @@
       parsed_command_line().GetSwitchValueASCII(switches::kPasswordStore);
   // Forward the product name
   config->product_name = l10n_util::GetStringUTF8(IDS_PRODUCT_NAME);
-  // OSCrypt may target keyring, which requires calls from the main thread.
+  // OSCrypt may target backends, which require calls from specific threads.
   config->main_thread_runner = content::BrowserThread::GetTaskRunnerForThread(
       content::BrowserThread::UI);
+  config->dbus_task_runner = chrome::GetDBusTaskRunner();
   // OSCrypt can be disabled in a special settings file.
   config->should_use_preference =
       parsed_command_line().HasSwitch(switches::kEnableEncryptionSelection);
diff --git a/chrome/browser/chromeos/BUILD.gn b/chrome/browser/chromeos/BUILD.gn
index 2bfb1aa..d4d4e67 100644
--- a/chrome/browser/chromeos/BUILD.gn
+++ b/chrome/browser/chromeos/BUILD.gn
@@ -992,6 +992,9 @@
     "login/screens/terms_of_service_screen.cc",
     "login/screens/terms_of_service_screen.h",
     "login/screens/terms_of_service_screen_view.h",
+    "login/screens/update_required_screen.cc",
+    "login/screens/update_required_screen.h",
+    "login/screens/update_required_view.h",
     "login/screens/update_screen.cc",
     "login/screens/update_screen.h",
     "login/screens/update_view.h",
diff --git a/chrome/browser/chromeos/login/app_launch_signin_screen.cc b/chrome/browser/chromeos/login/app_launch_signin_screen.cc
index 8cbbd37..ee1e059 100644
--- a/chrome/browser/chromeos/login/app_launch_signin_screen.cc
+++ b/chrome/browser/chromeos/login/app_launch_signin_screen.cc
@@ -126,6 +126,10 @@
   NOTREACHED();
 }
 
+void AppLaunchSigninScreen::ShowUpdateRequiredScreen() {
+  NOTREACHED();
+}
+
 void AppLaunchSigninScreen::ShowWrongHWIDScreen() {
   NOTREACHED();
 }
diff --git a/chrome/browser/chromeos/login/app_launch_signin_screen.h b/chrome/browser/chromeos/login/app_launch_signin_screen.h
index 5750123..116acea 100644
--- a/chrome/browser/chromeos/login/app_launch_signin_screen.h
+++ b/chrome/browser/chromeos/login/app_launch_signin_screen.h
@@ -70,6 +70,7 @@
   void ShowEnableDebuggingScreen() override;
   void ShowKioskEnableScreen() override;
   void ShowKioskAutolaunchScreen() override;
+  void ShowUpdateRequiredScreen() override;
   void ShowWrongHWIDScreen() override;
   void SetWebUIHandler(LoginDisplayWebUIHandler* webui_handler) override;
   bool IsShowGuest() const override;
diff --git a/chrome/browser/chromeos/login/existing_user_controller.cc b/chrome/browser/chromeos/login/existing_user_controller.cc
index 73aea39..de5dd50 100644
--- a/chrome/browser/chromeos/login/existing_user_controller.cc
+++ b/chrome/browser/chromeos/login/existing_user_controller.cc
@@ -347,6 +347,9 @@
           kAccountsPrefDeviceLocalAccountAutoLoginDelay,
           base::Bind(&ExistingUserController::ConfigureAutoLogin,
                      base::Unretained(this)));
+  minimum_version_policy_handler_ =
+      std::make_unique<policy::MinimumVersionPolicyHandler>(cros_settings_);
+  minimum_version_policy_handler_->AddObserver(this);
 }
 
 void ExistingUserController::Init(const user_manager::UserList& users) {
@@ -466,11 +469,28 @@
 void ExistingUserController::OnArcKioskAppsChanged() {
   ConfigureAutoLogin();
 }
+
+////////////////////////////////////////////////////////////////////////////////
+// ExistingUserController, policy::MinimumVersionPolicyHandler::Observer
+// implementation:
+//
+
+void ExistingUserController::OnMinimumVersionStateChanged() {
+  if (is_login_in_progress_) {
+    // Too late, but there is another check in user session.
+    return;
+  }
+  if (!minimum_version_policy_handler_->RequirementsAreSatisfied()) {
+    ShowUpdateRequiredScreen();
+  }
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // ExistingUserController, private:
 
 ExistingUserController::~ExistingUserController() {
   UserSessionManager::GetInstance()->DelegateDeleted(this);
+  minimum_version_policy_handler_->RemoveObserver(this);
 
   if (current_controller_ == this) {
     current_controller_ = nullptr;
@@ -690,6 +710,10 @@
   host_->StartWizard(OobeScreen::SCREEN_WRONG_HWID);
 }
 
+void ExistingUserController::ShowUpdateRequiredScreen() {
+  host_->StartWizard(OobeScreen::SCREEN_UPDATE_REQUIRED);
+}
+
 void ExistingUserController::Signout() {
   NOTREACHED();
 }
@@ -1563,6 +1587,7 @@
     login_display_->SetUIEnabled(true);
     return;
   }
+  //  if ()
 
   chromeos::DBusThreadManager::Get()
       ->GetCryptohomeClient()
diff --git a/chrome/browser/chromeos/login/existing_user_controller.h b/chrome/browser/chromeos/login/existing_user_controller.h
index c50920b..e598523 100644
--- a/chrome/browser/chromeos/login/existing_user_controller.h
+++ b/chrome/browser/chromeos/login/existing_user_controller.h
@@ -24,6 +24,7 @@
 #include "chrome/browser/chromeos/login/session/user_session_manager.h"
 #include "chrome/browser/chromeos/login/signin/token_handle_util.h"
 #include "chrome/browser/chromeos/login/ui/login_display.h"
+#include "chrome/browser/chromeos/policy/minimum_version_policy_handler.h"
 #include "chrome/browser/chromeos/policy/pre_signin_policy_fetcher.h"
 #include "chrome/browser/chromeos/settings/cros_settings.h"
 #include "chrome/browser/chromeos/settings/device_settings_service.h"
@@ -67,7 +68,8 @@
       public content::NotificationObserver,
       public LoginPerformer::Delegate,
       public UserSessionManagerDelegate,
-      public ArcKioskAppManager::ArcKioskAppManagerObserver {
+      public ArcKioskAppManager::ArcKioskAppManagerObserver,
+      public policy::MinimumVersionPolicyHandler::Observer {
  public:
   // All UI initialization is deferred till Init() call.
   explicit ExistingUserController(LoginDisplayHost* host);
@@ -107,6 +109,7 @@
   void SetDisplayAndGivenName(const std::string& display_name,
                               const std::string& given_name) override;
   void ShowWrongHWIDScreen() override;
+  void ShowUpdateRequiredScreen() override;
   void Signout() override;
   bool IsUserWhitelisted(const AccountId& account_id) override;
 
@@ -118,6 +121,9 @@
   // ArcKioskAppManager::ArcKioskAppManagerObserver overrides.
   void OnArcKioskAppsChanged() override;
 
+  // policy::MinimumVersionPolicyHandler::Observer overrides.
+  void OnMinimumVersionStateChanged() override;
+
   // Set a delegate that we will pass AuthStatusConsumer events to.
   // Used for testing.
   void set_login_status_consumer(AuthStatusConsumer* consumer) {
@@ -404,6 +410,8 @@
       local_account_auto_login_id_subscription_;
   std::unique_ptr<CrosSettings::ObserverSubscription>
       local_account_auto_login_delay_subscription_;
+  std::unique_ptr<policy::MinimumVersionPolicyHandler>
+      minimum_version_policy_handler_;
 
   std::unique_ptr<OAuth2TokenInitializer> oauth2_token_initializer_;
 
diff --git a/chrome/browser/chromeos/login/lock/webui_screen_locker.cc b/chrome/browser/chromeos/login/lock/webui_screen_locker.cc
index bf6819d..124f663 100644
--- a/chrome/browser/chromeos/login/lock/webui_screen_locker.cc
+++ b/chrome/browser/chromeos/login/lock/webui_screen_locker.cc
@@ -355,6 +355,10 @@
   NOTREACHED();
 }
 
+void WebUIScreenLocker::ShowUpdateRequiredScreen() {
+  NOTREACHED();
+}
+
 void WebUIScreenLocker::ResetAutoLoginTimer() {}
 
 void WebUIScreenLocker::ResyncUserData() {
diff --git a/chrome/browser/chromeos/login/lock/webui_screen_locker.h b/chrome/browser/chromeos/login/lock/webui_screen_locker.h
index 048a31a..363af938 100644
--- a/chrome/browser/chromeos/login/lock/webui_screen_locker.h
+++ b/chrome/browser/chromeos/login/lock/webui_screen_locker.h
@@ -99,6 +99,7 @@
   void OnStartKioskEnableScreen() override;
   void OnStartKioskAutolaunchScreen() override;
   void ShowWrongHWIDScreen() override;
+  void ShowUpdateRequiredScreen() override;
   void ResetAutoLoginTimer() override;
   void ResyncUserData() override;
   void SetDisplayEmail(const std::string& email) override;
diff --git a/chrome/browser/chromeos/login/oobe_screen.cc b/chrome/browser/chromeos/login/oobe_screen.cc
index 60a8b6f..0230cfdb 100644
--- a/chrome/browser/chromeos/login/oobe_screen.cc
+++ b/chrome/browser/chromeos/login/oobe_screen.cc
@@ -50,6 +50,7 @@
     "encryption-migration",          // SCREEN_ENCRYPTION_MIGRATION
     "voice-interaction-value-prop",  // SCREEN_VOICE_INTERACTION_VALUE_PROP
     "wait-for-container-ready",      // SCREEN_WAIT_FOR_CONTAINTER_READY
+    "update-required",               // SCREEN_UPDATE_REQUIRED
     "login",                         // SCREEN_SPECIAL_LOGIN
     "oobe",                          // SCREEN_SPECIAL_OOBE
     "test:nowindow",                 // SCREEN_TEST_NO_WINDOW
diff --git a/chrome/browser/chromeos/login/oobe_screen.h b/chrome/browser/chromeos/login/oobe_screen.h
index b305d3e..07cde09 100644
--- a/chrome/browser/chromeos/login/oobe_screen.h
+++ b/chrome/browser/chromeos/login/oobe_screen.h
@@ -48,6 +48,7 @@
   SCREEN_ENCRYPTION_MIGRATION,
   SCREEN_VOICE_INTERACTION_VALUE_PROP,
   SCREEN_WAIT_FOR_CONTAINER_READY,
+  SCREEN_UPDATE_REQUIRED,
 
   // Special "first screen" that initiates login flow.
   SCREEN_SPECIAL_LOGIN,
diff --git a/chrome/browser/chromeos/login/screens/update_required_screen.cc b/chrome/browser/chromeos/login/screens/update_required_screen.cc
new file mode 100644
index 0000000..f028409
--- /dev/null
+++ b/chrome/browser/chromeos/login/screens/update_required_screen.cc
@@ -0,0 +1,48 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/chromeos/login/screens/update_required_screen.h"
+
+#include <algorithm>
+
+#include "base/bind.h"
+#include "chrome/browser/chromeos/login/screens/base_screen_delegate.h"
+#include "chrome/browser/chromeos/login/screens/update_required_view.h"
+
+namespace chromeos {
+
+UpdateRequiredScreen::UpdateRequiredScreen(
+    BaseScreenDelegate* base_screen_delegate,
+    UpdateRequiredView* view)
+    : BaseScreen(base_screen_delegate, OobeScreen::SCREEN_UPDATE_REQUIRED),
+      view_(view),
+      weak_factory_(this) {
+  if (view_)
+    view_->Bind(this);
+}
+
+UpdateRequiredScreen::~UpdateRequiredScreen() {
+  if (view_)
+    view_->Unbind();
+}
+
+void UpdateRequiredScreen::OnViewDestroyed(UpdateRequiredView* view) {
+  if (view_ == view)
+    view_ = nullptr;
+}
+
+void UpdateRequiredScreen::Show() {
+  is_shown_ = true;
+
+  if (view_)
+    view_->Show();
+}
+
+void UpdateRequiredScreen::Hide() {
+  if (view_)
+    view_->Hide();
+  is_shown_ = false;
+}
+
+}  // namespace chromeos
diff --git a/chrome/browser/chromeos/login/screens/update_required_screen.h b/chrome/browser/chromeos/login/screens/update_required_screen.h
new file mode 100644
index 0000000..87714e9
--- /dev/null
+++ b/chrome/browser/chromeos/login/screens/update_required_screen.h
@@ -0,0 +1,50 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_CHROMEOS_LOGIN_SCREENS_UPDATE_REQUIRED_SCREEN_H_
+#define CHROME_BROWSER_CHROMEOS_LOGIN_SCREENS_UPDATE_REQUIRED_SCREEN_H_
+
+#include <set>
+
+#include "base/callback.h"
+#include "base/compiler_specific.h"
+#include "base/macros.h"
+#include "base/memory/weak_ptr.h"
+#include "chrome/browser/chromeos/login/screens/base_screen.h"
+#include "chrome/browser/chromeos/login/screens/error_screen.h"
+
+namespace chromeos {
+
+class BaseScreenDelegate;
+class UpdateRequiredView;
+
+// Controller for the update required screen.
+class UpdateRequiredScreen : public BaseScreen {
+ public:
+  constexpr static OobeScreen kScreenId = OobeScreen::SCREEN_UPDATE_REQUIRED;
+
+  UpdateRequiredScreen(BaseScreenDelegate* base_screen_delegate,
+                       UpdateRequiredView* view);
+  ~UpdateRequiredScreen() override;
+
+  // Called when the being destroyed. This should call Unbind() on the
+  // associated View if this class is destroyed before it.
+  void OnViewDestroyed(UpdateRequiredView* view);
+
+ private:
+  // BaseScreen:
+  void Show() override;
+  void Hide() override;
+
+  UpdateRequiredView* view_ = nullptr;
+  bool is_shown_;
+
+  base::WeakPtrFactory<UpdateRequiredScreen> weak_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(UpdateRequiredScreen);
+};
+
+}  // namespace chromeos
+
+#endif  // CHROME_BROWSER_CHROMEOS_LOGIN_SCREENS_UPDATE_REQUIRED_SCREEN_H_
diff --git a/chrome/browser/chromeos/login/screens/update_required_view.h b/chrome/browser/chromeos/login/screens/update_required_view.h
new file mode 100644
index 0000000..afa3a432
--- /dev/null
+++ b/chrome/browser/chromeos/login/screens/update_required_view.h
@@ -0,0 +1,38 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_CHROMEOS_LOGIN_SCREENS_UPDATE_REQUIRED_VIEW_H_
+#define CHROME_BROWSER_CHROMEOS_LOGIN_SCREENS_UPDATE_REQUIRED_VIEW_H_
+
+#include "chrome/browser/chromeos/login/oobe_screen.h"
+
+namespace chromeos {
+
+class UpdateRequiredScreen;
+
+// Interface for dependency injection between UpdateRequiredScreen and its
+// WebUI representation.
+
+class UpdateRequiredView {
+ public:
+  constexpr static OobeScreen kScreenId = OobeScreen::SCREEN_UPDATE_REQUIRED;
+
+  virtual ~UpdateRequiredView() {}
+
+  // Shows the contents of the screen.
+  virtual void Show() = 0;
+
+  // Hides the contents of the screen.
+  virtual void Hide() = 0;
+
+  // Binds |screen| to the view.
+  virtual void Bind(UpdateRequiredScreen* screen) = 0;
+
+  // Unbinds the screen from the view.
+  virtual void Unbind() = 0;
+};
+
+}  // namespace chromeos
+
+#endif  // CHROME_BROWSER_CHROMEOS_LOGIN_SCREENS_UPDATE_REQUIRED_VIEW_H_
diff --git a/chrome/browser/chromeos/login/ui/login_display.h b/chrome/browser/chromeos/login/ui/login_display.h
index ca7f8d1..052381f 100644
--- a/chrome/browser/chromeos/login/ui/login_display.h
+++ b/chrome/browser/chromeos/login/ui/login_display.h
@@ -81,6 +81,9 @@
     // Called when the owner permission for kiosk app auto launch is requested.
     virtual void OnStartKioskAutolaunchScreen() = 0;
 
+    // Shows update required screen.
+    virtual void ShowUpdateRequiredScreen() = 0;
+
     // Shows wrong HWID screen.
     virtual void ShowWrongHWIDScreen() = 0;
 
diff --git a/chrome/browser/chromeos/login/ui/login_display_host_webui.h b/chrome/browser/chromeos/login/ui/login_display_host_webui.h
index 9156155..26c75bc 100644
--- a/chrome/browser/chromeos/login/ui/login_display_host_webui.h
+++ b/chrome/browser/chromeos/login/ui/login_display_host_webui.h
@@ -129,10 +129,10 @@
   // Overridden from ui::InputDeviceEventObserver
   void OnTouchscreenDeviceConfigurationChanged() override;
 
-  // Overriden from views::WidgetRemovalsObserver:
+  // Overridden from views::WidgetRemovalsObserver:
   void OnWillRemoveView(views::Widget* widget, views::View* view) override;
 
-  // Overriden from MultiUserWindowManager::Observer:
+  // Overridden from chrome::MultiUserWindowManager::Observer:
   void OnUserSwitchAnimationFinished() override;
 
  private:
diff --git a/chrome/browser/chromeos/login/ui/webui_login_display.cc b/chrome/browser/chromeos/login/ui/webui_login_display.cc
index 952ee421..d5bc901d 100644
--- a/chrome/browser/chromeos/login/ui/webui_login_display.cc
+++ b/chrome/browser/chromeos/login/ui/webui_login_display.cc
@@ -272,6 +272,11 @@
     delegate_->OnStartKioskAutolaunchScreen();
 }
 
+void WebUILoginDisplay::ShowUpdateRequiredScreen() {
+  if (delegate_)
+    delegate_->ShowUpdateRequiredScreen();
+}
+
 void WebUILoginDisplay::ShowWrongHWIDScreen() {
   if (delegate_)
     delegate_->ShowWrongHWIDScreen();
diff --git a/chrome/browser/chromeos/login/ui/webui_login_display.h b/chrome/browser/chromeos/login/ui/webui_login_display.h
index 0ae1818f..37039c1 100644
--- a/chrome/browser/chromeos/login/ui/webui_login_display.h
+++ b/chrome/browser/chromeos/login/ui/webui_login_display.h
@@ -73,6 +73,7 @@
   void ShowEnableDebuggingScreen() override;
   void ShowKioskEnableScreen() override;
   void ShowKioskAutolaunchScreen() override;
+  void ShowUpdateRequiredScreen() override;
   void ShowWrongHWIDScreen() override;
   void SetWebUIHandler(LoginDisplayWebUIHandler* webui_handler) override;
   virtual void ShowSigninScreenForCreds(const std::string& username,
diff --git a/chrome/browser/chromeos/login/wizard_controller.cc b/chrome/browser/chromeos/login/wizard_controller.cc
index 3bb6b07..dc33eb2 100644
--- a/chrome/browser/chromeos/login/wizard_controller.cc
+++ b/chrome/browser/chromeos/login/wizard_controller.cc
@@ -51,6 +51,7 @@
 #include "chrome/browser/chromeos/login/screens/network_view.h"
 #include "chrome/browser/chromeos/login/screens/reset_screen.h"
 #include "chrome/browser/chromeos/login/screens/terms_of_service_screen.h"
+#include "chrome/browser/chromeos/login/screens/update_required_screen.h"
 #include "chrome/browser/chromeos/login/screens/update_screen.h"
 #include "chrome/browser/chromeos/login/screens/user_image_screen.h"
 #include "chrome/browser/chromeos/login/screens/voice_interaction_value_prop_screen.h"
@@ -451,8 +452,10 @@
   } else if (screen == OobeScreen::SCREEN_WAIT_FOR_CONTAINER_READY) {
     return new WaitForContainerReadyScreen(
         this, oobe_ui_->GetWaitForContainerReadyScreenView());
+  } else if (screen == OobeScreen::SCREEN_UPDATE_REQUIRED) {
+    return new UpdateRequiredScreen(this,
+                                    oobe_ui_->GetUpdateRequiredScreenView());
   }
-
   return nullptr;
 }
 
@@ -672,6 +675,10 @@
   SetCurrentScreen(GetScreen(OobeScreen::SCREEN_WAIT_FOR_CONTAINER_READY));
 }
 
+void WizardController::ShowUpdateRequiredScreen() {
+  SetCurrentScreen(GetScreen(OobeScreen::SCREEN_UPDATE_REQUIRED));
+}
+
 void WizardController::SkipToLoginForTesting(
     const LoginScreenContext& context) {
   VLOG(1) << "SkipToLoginForTesting.";
@@ -1166,6 +1173,8 @@
     ShowVoiceInteractionValuePropScreen();
   } else if (screen == OobeScreen::SCREEN_WAIT_FOR_CONTAINER_READY) {
     ShowWaitForContainerReadyScreen();
+  } else if (screen == OobeScreen::SCREEN_UPDATE_REQUIRED) {
+    ShowUpdateRequiredScreen();
   } else if (screen != OobeScreen::SCREEN_TEST_NO_WINDOW) {
     if (is_out_of_box_) {
       time_oobe_started_ = base::Time::Now();
diff --git a/chrome/browser/chromeos/login/wizard_controller.h b/chrome/browser/chromeos/login/wizard_controller.h
index d968f3b..68276f8 100644
--- a/chrome/browser/chromeos/login/wizard_controller.h
+++ b/chrome/browser/chromeos/login/wizard_controller.h
@@ -157,6 +157,7 @@
   void ShowEncryptionMigrationScreen();
   void ShowVoiceInteractionValuePropScreen();
   void ShowWaitForContainerReadyScreen();
+  void ShowUpdateRequiredScreen();
 
   // Shows images login screen.
   void ShowLoginScreen(const LoginScreenContext& context);
diff --git a/chrome/browser/loader/chrome_resource_dispatcher_host_delegate.cc b/chrome/browser/loader/chrome_resource_dispatcher_host_delegate.cc
index 4b78f19..1e8fc59 100644
--- a/chrome/browser/loader/chrome_resource_dispatcher_host_delegate.cc
+++ b/chrome/browser/loader/chrome_resource_dispatcher_host_delegate.cc
@@ -926,12 +926,9 @@
   if (data_reduction_proxy_io_data && previews_io_data) {
     previews::PreviewsUserData::Create(url_request,
                                        previews_io_data->GeneratePageId());
-    if (data_reduction_proxy_io_data->ShouldEnableLoFi(*url_request,
-                                                       previews_io_data)) {
+    if (data_reduction_proxy_io_data->ShouldEnableServerPreviews(
+            *url_request, previews_io_data)) {
       previews_state |= content::SERVER_LOFI_ON;
-    }
-    if (data_reduction_proxy_io_data->ShouldEnableLitePages(*url_request,
-                                                            previews_io_data)) {
       previews_state |= content::SERVER_LITE_PAGE_ON;
     }
 
diff --git a/chrome/browser/media_router_resources.grdp b/chrome/browser/media_router_resources.grdp
index 01200e3..4d61f45 100644
--- a/chrome/browser/media_router_resources.grdp
+++ b/chrome/browser/media_router_resources.grdp
@@ -30,9 +30,6 @@
   <include name="IDR_ROUTE_CONTROLS_HTML" file="resources\media_router\elements\route_controls\route_controls.html" type="BINDATA" />
   <include name="IDR_ROUTE_CONTROLS_CSS" file="resources\media_router\elements\route_controls\route_controls.css" type="BINDATA" />
   <include name="IDR_ROUTE_CONTROLS_JS" file="resources\media_router\elements\route_controls\route_controls.js" type="BINDATA" />
-  <include name="IDR_EXTENSION_VIEW_WRAPPER_HTML" file="resources\media_router\elements\route_details\extension_view_wrapper\extension_view_wrapper.html" type="BINDATA" />
-  <include name="IDR_EXTENSION_VIEW_WRAPPER_CSS" file="resources\media_router\elements\route_details\extension_view_wrapper\extension_view_wrapper.css" type="BINDATA" />
-  <include name="IDR_EXTENSION_VIEW_WRAPPER_JS" file="resources\media_router\elements\route_details\extension_view_wrapper\extension_view_wrapper.js" type="BINDATA" />
   <include name="IDR_ROUTE_DETAILS_HTML" file="resources\media_router\elements\route_details\route_details.html" type="BINDATA" />
   <include name="IDR_ROUTE_DETAILS_CSS" file="resources\media_router\elements\route_details\route_details.css" type="BINDATA" />
   <include name="IDR_ROUTE_DETAILS_JS" file="resources\media_router\elements\route_details\route_details.js" type="BINDATA" />
diff --git a/chrome/browser/resources/chromeos/login/custom_elements_login.html b/chrome/browser/resources/chromeos/login/custom_elements_login.html
index 6a1fdf5..d7ef61c 100644
--- a/chrome/browser/resources/chromeos/login/custom_elements_login.html
+++ b/chrome/browser/resources/chromeos/login/custom_elements_login.html
@@ -16,6 +16,7 @@
 <include src="throbber_notice.html">
 <include src="navigation_bar.html">
 <include src="unrecoverable_cryptohome_error_card.html">
+<include src="update_required_card.html">
 <include src="offline_ad_login.html">
 <include src="active_directory_password_change.html">
 <include src="arc_terms_of_service.html">
diff --git a/chrome/browser/resources/chromeos/login/custom_elements_login.js b/chrome/browser/resources/chromeos/login/custom_elements_login.js
index dc682da..d2937fd 100644
--- a/chrome/browser/resources/chromeos/login/custom_elements_login.js
+++ b/chrome/browser/resources/chromeos/login/custom_elements_login.js
@@ -16,6 +16,7 @@
 // <include src="throbber_notice.js">
 // <include src="navigation_bar.js">
 // <include src="unrecoverable_cryptohome_error_card.js">
+// <include src="update_required_card.js">
 // <include src="offline_ad_login.js">
 // <include src="active_directory_password_change.js">
 // <include src="oobe_buttons.js">
diff --git a/chrome/browser/resources/chromeos/login/login.js b/chrome/browser/resources/chromeos/login/login.js
index 18af9ca..5e1858b 100644
--- a/chrome/browser/resources/chromeos/login/login.js
+++ b/chrome/browser/resources/chromeos/login/login.js
@@ -54,6 +54,7 @@
       login.EncryptionMigrationScreen.register();
       login.VoiceInteractionValuePropScreen.register();
       login.WaitForContainerReadyScreen.register();
+      login.UpdateRequiredScreen.register();
 
       cr.ui.Bubble.decorate($('bubble'));
       login.HeaderBar.decorate($('login-header-bar'));
diff --git a/chrome/browser/resources/chromeos/login/login_non_lock_shared.html b/chrome/browser/resources/chromeos/login/login_non_lock_shared.html
index 2cc0e12..c77d3f7 100644
--- a/chrome/browser/resources/chromeos/login/login_non_lock_shared.html
+++ b/chrome/browser/resources/chromeos/login/login_non_lock_shared.html
@@ -36,5 +36,6 @@
 <link rel="stylesheet" href="screen_device_disabled.css">
 <link rel="stylesheet" href="screen_unrecoverable_cryptohome_error.css">
 <link rel="stylesheet" href="screen_active_directory_password_change.css">
+<link rel="stylesheet" href="screen_update_required.css">
 
 <script src="chrome://oobe/keyboard_utils.js"></script>
diff --git a/chrome/browser/resources/chromeos/login/login_non_lock_shared.js b/chrome/browser/resources/chromeos/login/login_non_lock_shared.js
index 9da4f9a2..765a6bec 100644
--- a/chrome/browser/resources/chromeos/login/login_non_lock_shared.js
+++ b/chrome/browser/resources/chromeos/login/login_non_lock_shared.js
@@ -34,6 +34,7 @@
 // <include src="screen_unrecoverable_cryptohome_error.js">
 // <include src="screen_active_directory_password_change.js">
 // <include src="screen_encryption_migration.js">
+// <include src="screen_update_required.js">
 
 // <include src="../../gaia_auth_host/authenticator.js">
 
diff --git a/chrome/browser/resources/chromeos/login/login_screens.html b/chrome/browser/resources/chromeos/login/login_screens.html
index 85a72af..f32accf 100644
--- a/chrome/browser/resources/chromeos/login/login_screens.html
+++ b/chrome/browser/resources/chromeos/login/login_screens.html
@@ -21,3 +21,4 @@
 <include src="screen_unrecoverable_cryptohome_error.html">
 <include src="screen_active_directory_password_change.html">
 <include src="screen_encryption_migration.html">
+<include src="screen_update_required.html">
diff --git a/chrome/browser/resources/chromeos/login/md_login.js b/chrome/browser/resources/chromeos/login/md_login.js
index 112bf30..c5b1e54 100644
--- a/chrome/browser/resources/chromeos/login/md_login.js
+++ b/chrome/browser/resources/chromeos/login/md_login.js
@@ -54,6 +54,7 @@
       login.EncryptionMigrationScreen.register();
       login.VoiceInteractionValuePropScreen.register();
       login.WaitForContainerReadyScreen.register();
+      login.UpdateRequiredScreen.register();
 
       cr.ui.Bubble.decorate($('bubble'));
       login.HeaderBar.decorate($('login-header-bar'));
diff --git a/chrome/browser/resources/chromeos/login/md_login_screens.html b/chrome/browser/resources/chromeos/login/md_login_screens.html
index 06db7cf..7ef0aff 100644
--- a/chrome/browser/resources/chromeos/login/md_login_screens.html
+++ b/chrome/browser/resources/chromeos/login/md_login_screens.html
@@ -21,3 +21,4 @@
 <include src="screen_unrecoverable_cryptohome_error.html">
 <include src="screen_active_directory_password_change.html">
 <include src="screen_encryption_migration.html">
+<include src="screen_update_required.html">
diff --git a/chrome/browser/resources/chromeos/login/oobe_screens.html b/chrome/browser/resources/chromeos/login/oobe_screens.html
index 7b3f79ed..47071ba 100644
--- a/chrome/browser/resources/chromeos/login/oobe_screens.html
+++ b/chrome/browser/resources/chromeos/login/oobe_screens.html
@@ -27,3 +27,4 @@
 <include src="screen_fatal_error.html">
 <include src="screen_device_disabled.html">
 <include src="screen_active_directory_password_change.html">
+<include src="screen_update_required.html">
diff --git a/chrome/browser/resources/media_router/elements/route_details/extension_view_wrapper/extension_view_wrapper.css b/chrome/browser/resources/chromeos/login/screen_update_required.css
similarity index 68%
rename from chrome/browser/resources/media_router/elements/route_details/extension_view_wrapper/extension_view_wrapper.css
rename to chrome/browser/resources/chromeos/login/screen_update_required.css
index bd41df1..086ea95 100644
--- a/chrome/browser/resources/media_router/elements/route_details/extension_view_wrapper/extension_view_wrapper.css
+++ b/chrome/browser/resources/chromeos/login/screen_update_required.css
@@ -2,8 +2,6 @@
  * Use of this source code is governed by a BSD-style license that can be
  * found in the LICENSE file. */
 
-#custom-controller {
-  display: inline-block;
-  height: 142px;
-  width: 100%;
+#update-required {
+  width: 448px; /* Should be the same as #gaia-signin. */
 }
diff --git a/chrome/browser/resources/chromeos/login/screen_update_required.html b/chrome/browser/resources/chromeos/login/screen_update_required.html
new file mode 100644
index 0000000..f5a72042
--- /dev/null
+++ b/chrome/browser/resources/chromeos/login/screen_update_required.html
@@ -0,0 +1,9 @@
+<!-- Copyright 2017 The Chromium Authors. All rights reserved.
+     Use of this source code is governed by a BSD-style license that can be
+     found in the LICENSE file. -->
+
+<div class="step faded no-logo" id="update-required" hidden>
+    <update-required-card id="update-required-card">
+    </update-required-card>
+</div>
+
diff --git a/chrome/browser/resources/chromeos/login/screen_update_required.js b/chrome/browser/resources/chromeos/login/screen_update_required.js
new file mode 100644
index 0000000..e454e32
--- /dev/null
+++ b/chrome/browser/resources/chromeos/login/screen_update_required.js
@@ -0,0 +1,16 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview "Update is required to sign in" screen.
+ */
+
+login.createScreen('UpdateRequiredScreen', 'update-required', function() {
+  return {
+    /** @Override */
+    onBeforeShow: function(data) {
+      Oobe.getInstance().headerHidden = true;
+    }
+  };
+});
diff --git a/chrome/browser/resources/chromeos/login/update_required_card.css b/chrome/browser/resources/chromeos/login/update_required_card.css
new file mode 100644
index 0000000..dccd287
--- /dev/null
+++ b/chrome/browser/resources/chromeos/login/update_required_card.css
@@ -0,0 +1,12 @@
+/* Copyright 2016 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file. */
+
+:host .content {
+  padding: 24px 24px 16px;
+}
+
+:host .message {
+  margin-bottom: 25px;
+  margin-top: 20px;
+}
diff --git a/chrome/browser/resources/chromeos/login/update_required_card.html b/chrome/browser/resources/chromeos/login/update_required_card.html
new file mode 100644
index 0000000..d581a88e0
--- /dev/null
+++ b/chrome/browser/resources/chromeos/login/update_required_card.html
@@ -0,0 +1,23 @@
+<!-- Copyright 2016 The Chromium Authors. All rights reserved.
+     Use of this source code is governed by a BSD-style license that can be
+     found in the LICENSE file. -->
+
+<link rel="import" href="chrome://resources/polymer/v1_0/polymer/polymer.html">
+
+<!--
+  Update required card that informs user that current chromeos version does not
+  satisfy policy requirements.
+
+  Events: none
+-->
+<dom-module id="update-required-card">
+  <link rel="stylesheet" href="oobe_flex_layout.css">
+  <link rel="stylesheet" href="update_required_card.css">
+
+  <template>
+    <div class="content">
+      <div class="message" i18n-content="updateRequiredMessage">
+      </div>
+    </div>
+  </template>
+</dom-module>
diff --git a/chrome/browser/resources/chromeos/login/update_required_card.js b/chrome/browser/resources/chromeos/login/update_required_card.js
new file mode 100644
index 0000000..ad7e012
--- /dev/null
+++ b/chrome/browser/resources/chromeos/login/update_required_card.js
@@ -0,0 +1,7 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+Polymer({
+  is: 'update-required-card',
+});
diff --git a/chrome/browser/resources/media_router/elements/route_controls/route_controls.css b/chrome/browser/resources/media_router/elements/route_controls/route_controls.css
index 0fcf48a..c2e56bd6 100644
--- a/chrome/browser/resources/media_router/elements/route_controls/route_controls.css
+++ b/chrome/browser/resources/media_router/elements/route_controls/route_controls.css
@@ -60,7 +60,6 @@
 #route-title {
   color: rgb(125, 125, 125);
   margin: 3px 8px;
-  overflow: hidden;
 }
 
 #route-volume-slider {
diff --git a/chrome/browser/resources/media_router/elements/route_controls/route_controls.html b/chrome/browser/resources/media_router/elements/route_controls/route_controls.html
index f6184a8..b9ecc9eb 100644
--- a/chrome/browser/resources/media_router/elements/route_controls/route_controls.html
+++ b/chrome/browser/resources/media_router/elements/route_controls/route_controls.html
@@ -8,11 +8,6 @@
   <link rel="import" type="css" href="route_controls.css">
   <template>
     <div id="media-controls">
-      <!--
-        TODO(crbug.com/786208): Remove the div below and always render the
-        description in the details element.  And, possibly combine details and
-        controls elements.
-      -->
       <div class="ellipsis" id="route-description"
            title="[[routeDescription_]]">
         [[routeDescription_]]
diff --git a/chrome/browser/resources/media_router/elements/route_controls/route_controls.js b/chrome/browser/resources/media_router/elements/route_controls/route_controls.js
index ce15e1e..d84188e 100644
--- a/chrome/browser/resources/media_router/elements/route_controls/route_controls.js
+++ b/chrome/browser/resources/media_router/elements/route_controls/route_controls.js
@@ -21,16 +21,6 @@
     },
 
     /**
-     * The route description to display. Uses the media route description if
-     * none is provided by the media route status object.
-     * @private {string}
-     */
-    routeDescription_: {
-      type: String,
-      value: '',
-    },
-
-    /**
      * The volume shown in the volume control, between 0 and 1.
      * @private {number}
      */
@@ -50,15 +40,6 @@
     },
 
     /**
-     * Keep in sync with media remoting individual user setting.
-     * @private {boolean}
-     */
-    mediaRemotingEnabled_: {
-      type: Boolean,
-      value: true,
-    },
-
-    /**
      * The timestamp for when the initial media status was loaded.
      * @private {number}
      */
@@ -116,6 +97,15 @@
     },
 
     /**
+     * Keep in sync with media remoting individual user setting.
+     * @private {boolean}
+     */
+    mediaRemotingEnabled_: {
+      type: Boolean,
+      value: true,
+    },
+
+    /**
      * The route currently associated with this controller.
      * @type {?media_router.Route|undefined}
      */
@@ -125,6 +115,16 @@
     },
 
     /**
+     * The route description to display. Uses the media route description if
+     * none is provided by the media route status object.
+     * @private {string}
+     */
+    routeDescription_: {
+      type: String,
+      value: '',
+    },
+
+    /**
      * The timestamp for when the route details view was opened.
      * @type {number}
      */
diff --git a/chrome/browser/resources/media_router/elements/route_details/compiled_resources2.gyp b/chrome/browser/resources/media_router/elements/route_details/compiled_resources2.gyp
index ea2470c..d8eb228 100644
--- a/chrome/browser/resources/media_router/elements/route_details/compiled_resources2.gyp
+++ b/chrome/browser/resources/media_router/elements/route_details/compiled_resources2.gyp
@@ -6,7 +6,6 @@
     {
       'target_name': 'route_details',
       'dependencies': [
-        'extension_view_wrapper/compiled_resources2.gyp:extension_view_wrapper',
         '../../compiled_resources2.gyp:media_router_data',
         '../../compiled_resources2.gyp:media_router_ui_interface',
         '../route_controls/compiled_resources2.gyp:route_controls',
diff --git a/chrome/browser/resources/media_router/elements/route_details/extension_view_wrapper/compiled_resources2.gyp b/chrome/browser/resources/media_router/elements/route_details/extension_view_wrapper/compiled_resources2.gyp
deleted file mode 100644
index b7d8fe1d..0000000
--- a/chrome/browser/resources/media_router/elements/route_details/extension_view_wrapper/compiled_resources2.gyp
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright 2017 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-{
-  'targets': [
-    {
-      'target_name': 'extension_view_wrapper',
-      'dependencies': [
-        '../../../compiled_resources2.gyp:media_router_data',
-      ],
-      'includes': ['../../../../../../../third_party/closure_compiler/compile_js2.gypi'],
-    },
-  ],
-}
diff --git a/chrome/browser/resources/media_router/elements/route_details/extension_view_wrapper/extension_view_wrapper.html b/chrome/browser/resources/media_router/elements/route_details/extension_view_wrapper/extension_view_wrapper.html
deleted file mode 100644
index 9b33d90..0000000
--- a/chrome/browser/resources/media_router/elements/route_details/extension_view_wrapper/extension_view_wrapper.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<link rel="import" href="chrome://resources/html/polymer.html">
-<dom-module id="extension-view-wrapper">
-  <link rel="import" type="css" href="extension_view_wrapper.css">
-  <template>
-    <extensionview id="custom-controller">
-    </extensionview>
-  </template>
-  <script src="extension_view_wrapper.js"></script>
-</dom-module>
diff --git a/chrome/browser/resources/media_router/elements/route_details/extension_view_wrapper/extension_view_wrapper.js b/chrome/browser/resources/media_router/elements/route_details/extension_view_wrapper/extension_view_wrapper.js
deleted file mode 100644
index 8740e75..0000000
--- a/chrome/browser/resources/media_router/elements/route_details/extension_view_wrapper/extension_view_wrapper.js
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// This Polymer element shows the custom controller for a route using
-// extensionview.
-Polymer({
-  is: 'extension-view-wrapper',
-
-  properties: {
-    /**
-     * Whether the extension view is ready to be shown.
-     * @type {boolean}
-     */
-    isExtensionViewReady: {
-      type: Boolean,
-      value: false,
-      notify: true,
-    },
-
-    /**
-     * The route to show the custom controller for.
-     * @type {?media_router.Route|undefined}
-     */
-    route: {
-      type: Object,
-      observer: 'maybeLoadExtensionView_',
-    },
-
-    /**
-     * The timestamp for when the route details view was opened.
-     * @type {number}
-     */
-    routeDetailsOpenTime: {
-      type: Number,
-      value: 0,
-    },
-  },
-
-  /**
-   * @return {?string}
-   */
-  getCustomControllerPath_: function() {
-    if (!this.route || !this.route.customControllerPath) {
-      return null;
-    }
-    return this.route.customControllerPath +
-        '&requestTimestamp=' + this.routeDetailsOpenTime;
-  },
-
-  /**
-   * Loads the custom controller if the controller path for the current route is
-   * valid.
-   */
-  maybeLoadExtensionView_: function() {
-    /** @const */ var extensionview = this.$['custom-controller'];
-    /** @const */ var controllerPath = this.getCustomControllerPath_();
-
-    // Do nothing if the controller path doesn't exist or is already shown in
-    // the extension view.
-    if (!controllerPath || controllerPath == extensionview.src) {
-      return;
-    }
-
-    /** @const */ var that = this;
-    extensionview.load(controllerPath)
-        .then(
-            function() {
-              // Load was successful; show the custom controller.
-              that.isExtensionViewReady = true;
-            },
-            function() {
-              // Load was unsuccessful; fall back to default view.
-              that.isExtensionViewReady = false;
-            });
-  },
-});
diff --git a/chrome/browser/resources/media_router/elements/route_details/route_details.html b/chrome/browser/resources/media_router/elements/route_details/route_details.html
index 8d91b1be..a745fd2a 100644
--- a/chrome/browser/resources/media_router/elements/route_details/route_details.html
+++ b/chrome/browser/resources/media_router/elements/route_details/route_details.html
@@ -1,24 +1,16 @@
 <link rel="import" href="chrome://resources/html/polymer.html">
 <link rel="import" href="chrome://resources/html/i18n_behavior.html">
 <link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
-<link rel="import" href="extension_view_wrapper/extension_view_wrapper.html">
 <link rel="import" href="../route_controls/route_controls.html">
 <dom-module id="route-details">
   <link rel="import" type="css" href="../../media_router_common.css">
   <link rel="import" type="css" href="route_details.css">
   <template>
     <div class="ellipsis" id="route-description" title="[[routeDescription_]]"
-        hidden$="[[!shouldShowRouteDescription_(controllerType_)]]">
+        hidden$="[[shouldShowWebUiControls_(route)]]">
       [[routeDescription_]]
     </div>
-    <template is="dom-if" if="[[shouldAttemptLoadingExtensionView_(route)]]">
-      <extension-view-wrapper id="extension-view-wrapper" route="[[route]]"
-          route-details-open-time="[[openTime_]]"
-          is-extension-view-ready="{{isExtensionViewReady}}"
-          hidden$="[[!shouldShowExtensionView_(controllerType_)]]">
-      </extension-view-wrapper>
-    </template>
-    <template is="dom-if" if="[[shouldShowWebUiControls_(controllerType_)]]">
+    <template is="dom-if" if="[[shouldShowWebUiControls_(route)]]">
       <route-controls id="route-controls"
           route-details-open-time="[[openTime_]]"
           route="[[route]]"></route-controls>
diff --git a/chrome/browser/resources/media_router/elements/route_details/route_details.js b/chrome/browser/resources/media_router/elements/route_details/route_details.js
index 8724cd2..a91b589 100644
--- a/chrome/browser/resources/media_router/elements/route_details/route_details.js
+++ b/chrome/browser/resources/media_router/elements/route_details/route_details.js
@@ -28,15 +28,6 @@
     },
 
     /**
-     * An enum value to represent the controller to show.
-     * @private {number}
-     */
-    controllerType_: {
-      type: Number,
-      computed: 'computeControllerType_(route, isExtensionViewReady)',
-    },
-
-    /**
      * Whether a sink is currently launching in the container.
      * @type {boolean}
      */
@@ -46,15 +37,6 @@
     },
 
     /**
-     * Whether the custom controller extensionview is ready to be shown.
-     * @type {boolean}
-     */
-    isExtensionViewReady: {
-      type: Boolean,
-      value: false,
-    },
-
-    /**
      * The timestamp for when the route details view was opened. We initialize
      * the value in a function so that the value is set when the element is
      * loaded, rather than at page load.
@@ -150,22 +132,6 @@
   },
 
   /**
-   * @param {?media_router.Route} route
-   * @param {boolean} isExtensionViewReady
-   * @return {number} An enum value to represent the controller to show.
-   * @private
-   */
-  computeControllerType_: function(route, isExtensionViewReady) {
-    if (route && route.supportsWebUiController) {
-      return media_router.ControllerType.WEBUI;
-    }
-    if (isExtensionViewReady) {
-      return media_router.ControllerType.EXTENSION;
-    }
-    return media_router.ControllerType.NONE;
-  },
-
-  /**
    * @param {number} castMode User selected cast mode or AUTO.
    * @param {?media_router.Sink} sink Sink to which we will cast.
    * @return {number} The selected cast mode when |castMode| is selected in the
@@ -191,20 +157,10 @@
   },
 
   /**
-   * Updates |routeDescription_| for the default view.
-   *
-   * @private
-   */
-  updateRouteDescription_: function() {
-    this.routeDescription_ = this.route ? this.route.description : '';
-  },
-
-  /**
    * Called when the route details view is closed. Resets route-controls.
    */
   onClosed: function() {
-    if (this.controllerType_ === media_router.ControllerType.WEBUI &&
-        this.$$('route-controls')) {
+    if (this.$$('route-controls')) {
       this.$$('route-controls').reset();
     }
   },
@@ -213,62 +169,29 @@
    * Called when the route details view is opened.
    */
   onOpened: function() {
-    if (this.controllerType_ === media_router.ControllerType.WEBUI &&
-        this.$$('route-controls')) {
+    if (this.$$('route-controls')) {
       media_router.ui.setRouteControls(
           /** @type {RouteControlsInterface} */ (this.$$('route-controls')));
     }
   },
 
   /**
-   * Updates either the extensionview or the WebUI route controller, depending
-   * on which should be shown.
-   * @param {?media_router.Route} newRoute
+   * Updates |routeDescription_| for the default view.
+   * @param {?media_router.Route} route
    * @private
    */
-  onRouteChange_: function(newRoute) {
-    if (this.controllerType_ !== media_router.ControllerType.WEBUI) {
-      this.updateRouteDescription_();
-    }
+  onRouteChange_: function(route) {
+    this.routeDescription_ = route ? route.description : '';
   },
 
   /**
    * @param {?media_router.Route} route
-   * @return {boolean}
-   * @private
-   */
-  shouldAttemptLoadingExtensionView_: function(route) {
-    return !!route && !route.supportsWebUiController;
-  },
-
-  /**
-   * @param {number} controllerType
-   * @return {boolean} Whether the extensionview should be shown instead of the
-   *     default route info element or the WebUI route controller.
-   * @private
-   */
-  shouldShowExtensionView_: function(controllerType) {
-    return controllerType === media_router.ControllerType.EXTENSION;
-  },
-
-  /**
-   * @param {number} controllerType
-   * @return {boolean} Whether the route info element should be shown instead of
-   *     the extensionview or the WebUI route controller.
-   * @private
-   */
-  shouldShowRouteDescription_: function(controllerType) {
-    return controllerType === media_router.ControllerType.NONE;
-  },
-
-  /**
-   * @param {number} controllerType
    * @return {boolean} Whether the WebUI route controller should be shown
-   *     instead of the default route info element or the extensionview.
+   *     instead of the default route description element.
    * @private
    */
-  shouldShowWebUiControls_: function(controllerType) {
-    return controllerType === media_router.ControllerType.WEBUI;
+  shouldShowWebUiControls_: function(route) {
+    return route && route.supportsWebUiController;
   },
 
   /**
diff --git a/chrome/browser/resources/media_router/media_router_common.css b/chrome/browser/resources/media_router/media_router_common.css
index d944684..ee07e9d 100644
--- a/chrome/browser/resources/media_router/media_router_common.css
+++ b/chrome/browser/resources/media_router/media_router_common.css
@@ -21,12 +21,8 @@
 }
 
 .ellipsis {
+  overflow: hidden;
   padding: 0 1%;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
-
-#route-description {
-  background-color: white;
-  overflow: hidden;
-}
diff --git a/chrome/browser/resources/media_router/media_router_data.js b/chrome/browser/resources/media_router/media_router_data.js
index bcb1b16..a3d4861f 100644
--- a/chrome/browser/resources/media_router/media_router_data.js
+++ b/chrome/browser/resources/media_router/media_router_data.js
@@ -25,16 +25,6 @@
 };
 
 /**
- * Route controller types that can be shown in the route details view.
- * @enum {number}
- */
-media_router.ControllerType = {
-  NONE: 0,
-  WEBUI: 1,
-  EXTENSION: 2,
-};
-
-/**
  * The ESC key maps to KeyboardEvent.key value 'Escape'.
  * @const {string}
  */
diff --git a/chrome/browser/safe_browsing/chrome_cleaner/chrome_cleaner_runner_win.cc b/chrome/browser/safe_browsing/chrome_cleaner/chrome_cleaner_runner_win.cc
index e33dc8a..de2be261 100644
--- a/chrome/browser/safe_browsing/chrome_cleaner/chrome_cleaner_runner_win.cc
+++ b/chrome/browser/safe_browsing/chrome_cleaner/chrome_cleaner_runner_win.cc
@@ -123,6 +123,11 @@
       chrome_cleaner::kEngineSwitch,
       reporter_engine.empty() ? "1" : reporter_engine);
 
+  if (reporter_invocation.cleaner_logs_upload_enabled()) {
+    cleaner_command_line_.AppendSwitch(
+        chrome_cleaner::kWithScanningModeLogsSwitch);
+  }
+
   // If metrics is enabled, we can enable crash reporting in the Chrome Cleaner
   // process.
   if (metrics_status == ChromeMetricsStatus::kEnabled) {
diff --git a/chrome/browser/safe_browsing/chrome_cleaner/chrome_cleaner_runner_win_unittest.cc b/chrome/browser/safe_browsing/chrome_cleaner/chrome_cleaner_runner_win_unittest.cc
index c4ebea6..1b1e3ed 100644
--- a/chrome/browser/safe_browsing/chrome_cleaner/chrome_cleaner_runner_win_unittest.cc
+++ b/chrome/browser/safe_browsing/chrome_cleaner/chrome_cleaner_runner_win_unittest.cc
@@ -56,17 +56,21 @@
 //       enabled
 // - reporter_engine (ReporterEngine): the type of Cleaner engine specified in
 //       the SwReporterInvocation.
+// - cleaner_logs_enabled (bool): if logs can be collected in the cleaner
+//       process running in scanning mode.
 class ChromeCleanerRunnerSimpleTest
     : public testing::TestWithParam<
           std::tuple<ChromeCleanerRunner::ChromeMetricsStatus,
-                     ReporterEngine>>,
+                     ReporterEngine,
+                     bool>>,
       public ChromeCleanerRunnerTestDelegate {
  public:
   ChromeCleanerRunnerSimpleTest()
       : command_line_(base::CommandLine::NO_PROGRAM) {}
 
   void SetUp() override {
-    std::tie(metrics_status_, reporter_engine_) = GetParam();
+    std::tie(metrics_status_, reporter_engine_, cleaner_logs_enabled_) =
+        GetParam();
 
     SetChromeCleanerRunnerTestDelegateForTesting(this);
   }
@@ -88,6 +92,8 @@
         break;
     }
 
+    reporter_invocation.set_cleaner_logs_upload_enabled(cleaner_logs_enabled_);
+
     ChromeCleanerRunner::RunChromeCleanerAndReplyWithExitCode(
         base::FilePath(FILE_PATH_LITERAL("cleaner.exe")), reporter_invocation,
         metrics_status_,
@@ -132,6 +138,7 @@
   // Test fixture parameters.
   ChromeCleanerRunner::ChromeMetricsStatus metrics_status_;
   ReporterEngine reporter_engine_;
+  bool cleaner_logs_enabled_ = false;
 
   // Set by LaunchTestProcess.
   base::CommandLine command_line_;
@@ -172,6 +179,9 @@
   EXPECT_EQ(
       metrics_status_ == ChromeMetricsStatus::kEnabled,
       command_line_.HasSwitch(chrome_cleaner::kEnableCrashReportingSwitch));
+  EXPECT_EQ(
+      cleaner_logs_enabled_,
+      command_line_.HasSwitch(chrome_cleaner::kWithScanningModeLogsSwitch));
 }
 
 INSTANTIATE_TEST_CASE_P(
@@ -181,7 +191,8 @@
                    ChromeCleanerRunner::ChromeMetricsStatus::kDisabled),
             Values(ReporterEngine::kUnspecified,
                    ReporterEngine::kOldEngine,
-                   ReporterEngine::kNewEngine)));
+                   ReporterEngine::kNewEngine),
+            Bool()));
 
 // Enum to be used as parameter for the ChromeCleanerRunnerTest fixture below.
 enum class UwsFoundState {
diff --git a/chrome/browser/safe_browsing/chrome_cleaner/reporter_runner_win.cc b/chrome/browser/safe_browsing/chrome_cleaner/reporter_runner_win.cc
index c29b2b0..a365947 100644
--- a/chrome/browser/safe_browsing/chrome_cleaner/reporter_runner_win.cc
+++ b/chrome/browser/safe_browsing/chrome_cleaner/reporter_runner_win.cc
@@ -522,7 +522,7 @@
 // Scans and shows the Chrome Cleaner UI if the user has not already been
 // prompted in the current prompt wave.
 void MaybeScanAndPrompt(SwReporterInvocationType invocation_type,
-                        const SwReporterInvocation& reporter_invocation) {
+                        SwReporterInvocation reporter_invocation) {
   ChromeCleanerController* cleaner_controller =
       ChromeCleanerController::GetInstance();
 
@@ -562,6 +562,10 @@
     return;
   }
 
+  reporter_invocation.set_cleaner_logs_upload_enabled(
+      invocation_type ==
+      SwReporterInvocationType::kUserInitiatedWithLogsAllowed);
+
   cleaner_controller->Scan(reporter_invocation);
   DCHECK_EQ(ChromeCleanerController::State::kScanning,
             cleaner_controller->state());
@@ -1009,6 +1013,15 @@
   reporter_logs_upload_enabled_ = reporter_logs_upload_enabled;
 }
 
+bool SwReporterInvocation::cleaner_logs_upload_enabled() const {
+  return cleaner_logs_upload_enabled_;
+}
+
+void SwReporterInvocation::set_cleaner_logs_upload_enabled(
+    bool cleaner_logs_upload_enabled) {
+  cleaner_logs_upload_enabled_ = cleaner_logs_upload_enabled;
+}
+
 SwReporterInvocationSequence::SwReporterInvocationSequence(
     const base::Version& version,
     const Queue& container,
diff --git a/chrome/browser/safe_browsing/chrome_cleaner/reporter_runner_win.h b/chrome/browser/safe_browsing/chrome_cleaner/reporter_runner_win.h
index 101c30d..a4dec16d 100644
--- a/chrome/browser/safe_browsing/chrome_cleaner/reporter_runner_win.h
+++ b/chrome/browser/safe_browsing/chrome_cleaner/reporter_runner_win.h
@@ -111,6 +111,11 @@
   bool reporter_logs_upload_enabled() const;
   void set_reporter_logs_upload_enabled(bool reporter_logs_upload_enabled);
 
+  // Indicates if the invocation type allows logs to be uploaded by the
+  // cleaner process in scanning mode.
+  bool cleaner_logs_upload_enabled() const;
+  void set_cleaner_logs_upload_enabled(bool cleaner_logs_upload_enabled);
+
  private:
   base::CommandLine command_line_;
 
@@ -119,6 +124,7 @@
   std::string suffix_;
 
   bool reporter_logs_upload_enabled_ = false;
+  bool cleaner_logs_upload_enabled_ = false;
 };
 
 enum class SwReporterInvocationResult {
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index 63385d3..26a7a14 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -1436,6 +1436,8 @@
       "webui/chromeos/login/supervised_user_creation_screen_handler.h",
       "webui/chromeos/login/terms_of_service_screen_handler.cc",
       "webui/chromeos/login/terms_of_service_screen_handler.h",
+      "webui/chromeos/login/update_required_screen_handler.cc",
+      "webui/chromeos/login/update_required_screen_handler.h",
       "webui/chromeos/login/update_screen_handler.cc",
       "webui/chromeos/login/update_screen_handler.h",
       "webui/chromeos/login/user_board_screen_handler.cc",
diff --git a/chrome/browser/ui/webui/chromeos/login/oobe_ui.cc b/chrome/browser/ui/webui/chromeos/login/oobe_ui.cc
index 353c154..e95c6d5d 100644
--- a/chrome/browser/ui/webui/chromeos/login/oobe_ui.cc
+++ b/chrome/browser/ui/webui/chromeos/login/oobe_ui.cc
@@ -57,6 +57,7 @@
 #include "chrome/browser/ui/webui/chromeos/login/signin_screen_handler.h"
 #include "chrome/browser/ui/webui/chromeos/login/supervised_user_creation_screen_handler.h"
 #include "chrome/browser/ui/webui/chromeos/login/terms_of_service_screen_handler.h"
+#include "chrome/browser/ui/webui/chromeos/login/update_required_screen_handler.h"
 #include "chrome/browser/ui/webui/chromeos/login/update_screen_handler.h"
 #include "chrome/browser/ui/webui/chromeos/login/user_board_screen_handler.h"
 #include "chrome/browser/ui/webui/chromeos/login/user_image_screen_handler.h"
@@ -341,6 +342,8 @@
 
   AddScreenHandler(base::MakeUnique<WaitForContainerReadyScreenHandler>());
 
+  AddScreenHandler(base::MakeUnique<UpdateRequiredScreenHandler>());
+
   // Initialize KioskAppMenuHandler. Note that it is NOT a screen handler.
   auto kiosk_app_menu_handler =
       base::MakeUnique<KioskAppMenuHandler>(network_state_informer_);
@@ -464,6 +467,10 @@
   return GetView<WaitForContainerReadyScreenHandler>();
 }
 
+UpdateRequiredView* OobeUI::GetUpdateRequiredScreenView() {
+  return GetView<UpdateRequiredScreenHandler>();
+}
+
 UserImageView* OobeUI::GetUserImageView() {
   return GetView<UserImageScreenHandler>();
 }
diff --git a/chrome/browser/ui/webui/chromeos/login/oobe_ui.h b/chrome/browser/ui/webui/chromeos/login/oobe_ui.h
index fb7ad5c..c0ad51d 100644
--- a/chrome/browser/ui/webui/chromeos/login/oobe_ui.h
+++ b/chrome/browser/ui/webui/chromeos/login/oobe_ui.h
@@ -58,6 +58,7 @@
 class UserBoardView;
 class UserImageView;
 class UpdateView;
+class UpdateRequiredView;
 class VoiceInteractionValuePropScreenView;
 class WaitForContainerReadyScreenView;
 class WrongHWIDScreenView;
@@ -119,6 +120,7 @@
   EncryptionMigrationScreenView* GetEncryptionMigrationScreenView();
   VoiceInteractionValuePropScreenView* GetVoiceInteractionValuePropScreenView();
   WaitForContainerReadyScreenView* GetWaitForContainerReadyScreenView();
+  UpdateRequiredView* GetUpdateRequiredScreenView();
   GaiaView* GetGaiaScreenView();
   UserBoardView* GetUserBoardView();
 
diff --git a/chrome/browser/ui/webui/chromeos/login/signin_screen_handler.cc b/chrome/browser/ui/webui/chromeos/login/signin_screen_handler.cc
index 848a9da..1a6188f 100644
--- a/chrome/browser/ui/webui/chromeos/login/signin_screen_handler.cc
+++ b/chrome/browser/ui/webui/chromeos/login/signin_screen_handler.cc
@@ -60,6 +60,7 @@
 #include "chrome/browser/chromeos/login/wizard_controller.h"
 #include "chrome/browser/chromeos/policy/browser_policy_connector_chromeos.h"
 #include "chrome/browser/chromeos/policy/device_local_account.h"
+#include "chrome/browser/chromeos/policy/minimum_version_policy_handler.h"
 #include "chrome/browser/chromeos/profiles/profile_helper.h"
 #include "chrome/browser/chromeos/settings/cros_settings.h"
 #include "chrome/browser/chromeos/system/system_clock.h"
@@ -159,6 +160,12 @@
   DISALLOW_COPY_AND_ASSIGN(CallOnReturn);
 };
 
+policy::MinimumVersionPolicyHandler* GetMinimumVersionPolicyHandler() {
+  return g_browser_process->platform_part()
+      ->browser_policy_connector_chromeos()
+      ->GetMinimumVersionPolicyHandler();
+}
+
 }  // namespace
 
 namespace chromeos {
@@ -1292,6 +1299,12 @@
     return;
   }
 
+  if (delegate_ && !oobe_ui_ && GetMinimumVersionPolicyHandler() &&
+      !GetMinimumVersionPolicyHandler()->RequirementsAreSatisfied()) {
+    delegate_->ShowUpdateRequiredScreen();
+    return;
+  }
+
   PrefService* prefs = g_browser_process->local_state();
   if (prefs->GetBoolean(prefs::kFactoryResetRequested)) {
     if (core_oobe_view_)
diff --git a/chrome/browser/ui/webui/chromeos/login/signin_screen_handler.h b/chrome/browser/ui/webui/chromeos/login/signin_screen_handler.h
index e4dd6c5..680ab51 100644
--- a/chrome/browser/ui/webui/chromeos/login/signin_screen_handler.h
+++ b/chrome/browser/ui/webui/chromeos/login/signin_screen_handler.h
@@ -170,6 +170,9 @@
   // Show wrong hwid screen.
   virtual void ShowWrongHWIDScreen() = 0;
 
+  // Show update required screen.
+  virtual void ShowUpdateRequiredScreen() = 0;
+
   // Sets the displayed email for the next login attempt. If it succeeds,
   // user's displayed email value will be updated to |email|.
   virtual void SetDisplayEmail(const std::string& email) = 0;
diff --git a/chrome/browser/ui/webui/chromeos/login/update_required_screen_handler.cc b/chrome/browser/ui/webui/chromeos/login/update_required_screen_handler.cc
new file mode 100644
index 0000000..acdac707
--- /dev/null
+++ b/chrome/browser/ui/webui/chromeos/login/update_required_screen_handler.cc
@@ -0,0 +1,67 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/webui/chromeos/login/update_required_screen_handler.h"
+
+#include <memory>
+
+#include "base/values.h"
+#include "chrome/browser/chromeos/login/oobe_screen.h"
+#include "chrome/browser/chromeos/login/screens/update_required_screen.h"
+#include "chrome/grit/chromium_strings.h"
+#include "chrome/grit/generated_resources.h"
+#include "components/login/localized_values_builder.h"
+
+namespace {
+
+const char kJsScreenPath[] = "login.UpdateRequiredScreen";
+
+}  // namespace
+
+namespace chromeos {
+
+UpdateRequiredScreenHandler::UpdateRequiredScreenHandler()
+    : BaseScreenHandler(kScreenId) {
+  set_call_js_prefix(kJsScreenPath);
+}
+
+UpdateRequiredScreenHandler::~UpdateRequiredScreenHandler() {
+  if (screen_)
+    screen_->OnViewDestroyed(this);
+}
+
+void UpdateRequiredScreenHandler::DeclareLocalizedValues(
+    ::login::LocalizedValuesBuilder* builder) {
+  builder->Add("updateRequiredMessage",
+               IDS_UPDATE_REQUIRED_LOGIN_SCREEN_MESSAGE);
+}
+
+void UpdateRequiredScreenHandler::Initialize() {
+  if (show_on_init_) {
+    Show();
+    show_on_init_ = false;
+  }
+}
+
+void UpdateRequiredScreenHandler::Show() {
+  if (!page_is_ready()) {
+    show_on_init_ = true;
+    return;
+  }
+  ShowScreen(kScreenId);
+}
+
+void UpdateRequiredScreenHandler::Hide() {}
+
+void UpdateRequiredScreenHandler::Bind(UpdateRequiredScreen* screen) {
+  screen_ = screen;
+  BaseScreenHandler::SetBaseScreen(screen_);
+}
+
+void UpdateRequiredScreenHandler::Unbind() {
+  screen_ = nullptr;
+  BaseScreenHandler::SetBaseScreen(nullptr);
+}
+
+}  // namespace chromeos
diff --git a/chrome/browser/ui/webui/chromeos/login/update_required_screen_handler.h b/chrome/browser/ui/webui/chromeos/login/update_required_screen_handler.h
new file mode 100644
index 0000000..a4d8083
--- /dev/null
+++ b/chrome/browser/ui/webui/chromeos/login/update_required_screen_handler.h
@@ -0,0 +1,45 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_WEBUI_CHROMEOS_LOGIN_UPDATE_REQUIRED_SCREEN_HANDLER_H_
+#define CHROME_BROWSER_UI_WEBUI_CHROMEOS_LOGIN_UPDATE_REQUIRED_SCREEN_HANDLER_H_
+
+#include <string>
+
+#include "base/compiler_specific.h"
+#include "base/macros.h"
+#include "chrome/browser/chromeos/login/screens/update_required_screen.h"
+#include "chrome/browser/chromeos/login/screens/update_required_view.h"
+#include "chrome/browser/ui/webui/chromeos/login/base_screen_handler.h"
+
+namespace chromeos {
+
+class UpdateRequiredScreenHandler : public UpdateRequiredView,
+                                    public BaseScreenHandler {
+ public:
+  UpdateRequiredScreenHandler();
+  ~UpdateRequiredScreenHandler() override;
+
+ private:
+  void Show() override;
+  void Hide() override;
+  void Bind(UpdateRequiredScreen* screen) override;
+  void Unbind() override;
+
+  // BaseScreenHandler:
+  void DeclareLocalizedValues(
+      ::login::LocalizedValuesBuilder* builder) override;
+  void Initialize() override;
+
+  UpdateRequiredScreen* screen_ = nullptr;
+
+  // If true, Initialize() will call Show().
+  bool show_on_init_ = false;
+
+  DISALLOW_COPY_AND_ASSIGN(UpdateRequiredScreenHandler);
+};
+
+}  // namespace chromeos
+
+#endif  // CHROME_BROWSER_UI_WEBUI_CHROMEOS_LOGIN_UPDATE_REQUIRED_SCREEN_HANDLER_H_
diff --git a/chrome/browser/ui/webui/media_router/media_router_resources_provider.cc b/chrome/browser/ui/webui/media_router/media_router_resources_provider.cc
index 79861a0..bd29d23 100644
--- a/chrome/browser/ui/webui/media_router/media_router_resources_provider.cc
+++ b/chrome/browser/ui/webui/media_router/media_router_resources_provider.cc
@@ -81,17 +81,6 @@
   html_source->AddResourcePath(
       "elements/media_router_container/pseudo_sink_search_state.js",
       IDR_PSEUDO_SINK_SEARCH_STATE_JS);
-  html_source->AddResourcePath(
-      "elements/route_details/extension_view_wrapper/"
-      "extension_view_wrapper.html",
-      IDR_EXTENSION_VIEW_WRAPPER_HTML);
-  html_source->AddResourcePath(
-      "elements/route_details/extension_view_wrapper/extension_view_wrapper.js",
-      IDR_EXTENSION_VIEW_WRAPPER_JS);
-  html_source->AddResourcePath(
-      "elements/route_details/extension_view_wrapper/"
-      "extension_view_wrapper.css",
-      IDR_EXTENSION_VIEW_WRAPPER_CSS);
 }
 
 }  // namespace
diff --git a/chrome/browser/ui/webui/media_router/media_router_webui_message_handler.cc b/chrome/browser/ui/webui/media_router/media_router_webui_message_handler.cc
index 2f6ac26..25b9963 100644
--- a/chrome/browser/ui/webui/media_router/media_router_webui_message_handler.cc
+++ b/chrome/browser/ui/webui/media_router/media_router_webui_message_handler.cc
@@ -158,17 +158,14 @@
     bool can_join,
     const std::string& extension_id,
     bool incognito,
-    int current_cast_mode,
-    bool is_web_ui_route_controller_available) {
+    int current_cast_mode) {
   auto dictionary = base::MakeUnique<base::DictionaryValue>();
   dictionary->SetString("id", route.media_route_id());
   dictionary->SetString("sinkId", route.media_sink_id());
   dictionary->SetString("description", route.description());
   dictionary->SetBoolean("isLocal", route.is_local());
-  dictionary->SetBoolean(
-      "supportsWebUiController",
-      is_web_ui_route_controller_available &&
-          route.controller_type() != RouteControllerType::kNone);
+  dictionary->SetBoolean("supportsWebUiController",
+                         route.controller_type() != RouteControllerType::kNone);
   dictionary->SetBoolean("canJoin", can_join);
   if (current_cast_mode > 0) {
     dictionary->SetInteger("currentCastMode", current_cast_mode);
@@ -261,8 +258,6 @@
     : incognito_(
           Profile::FromWebUI(media_router_ui->web_ui())->IsOffTheRecord()),
       dialog_closing_(false),
-      is_web_ui_route_controller_available_(base::FeatureList::IsEnabled(
-          features::kMediaRouterUIRouteController)),
       media_router_ui_(media_router_ui) {}
 
 MediaRouterWebUIMessageHandler::~MediaRouterWebUIMessageHandler() {}
@@ -305,7 +300,7 @@
         route->media_route_id(), media_router_ui_->routes_and_cast_modes());
     std::unique_ptr<base::DictionaryValue> route_value(RouteToValue(
         *route, false, media_router_ui_->GetRouteProviderExtensionId(),
-        incognito_, current_cast_mode, is_web_ui_route_controller_available_));
+        incognito_, current_cast_mode));
     web_ui()->CallJavascriptFunctionUnsafe(kOnCreateRouteResponseReceived,
                                            base::Value(sink_id), *route_value,
                                            base::Value(route->for_display()));
@@ -1132,9 +1127,8 @@
         base::ContainsValue(joinable_route_ids, route.media_route_id());
     int current_cast_mode =
         CurrentCastModeForRouteId(route.media_route_id(), current_cast_modes);
-    std::unique_ptr<base::DictionaryValue> route_val(
-        RouteToValue(route, can_join, extension_id, incognito_,
-                     current_cast_mode, is_web_ui_route_controller_available_));
+    std::unique_ptr<base::DictionaryValue> route_val(RouteToValue(
+        route, can_join, extension_id, incognito_, current_cast_mode));
     value->Append(std::move(route_val));
   }
 
diff --git a/chrome/browser/ui/webui/media_router/media_router_webui_message_handler.h b/chrome/browser/ui/webui/media_router/media_router_webui_message_handler.h
index dd82fa2a..43e0f44 100644
--- a/chrome/browser/ui/webui/media_router/media_router_webui_message_handler.h
+++ b/chrome/browser/ui/webui/media_router/media_router_webui_message_handler.h
@@ -166,9 +166,6 @@
   // Keeps track of whether a command to close the dialog has been issued.
   bool dialog_closing_;
 
-  // Whether the WebUI version of route controller is available for use.
-  const bool is_web_ui_route_controller_available_;
-
   // The media status currently shown in the UI.
   base::Optional<MediaStatus> current_media_status_;
 
diff --git a/chrome/common/chrome_features.cc b/chrome/common/chrome_features.cc
index c1e994b..984b4d49 100644
--- a/chrome/common/chrome_features.cc
+++ b/chrome/common/chrome_features.cc
@@ -329,12 +329,6 @@
 // during Cast Tab Mirroring.
 const base::Feature kMediaRemoting{"MediaRemoting",
                                    base::FEATURE_DISABLED_BY_DEFAULT};
-
-// If enabled, replaces the <extensionview> controller in the route details view
-// of the Media Router dialog with the controller bundled with the WebUI
-// resources.
-const base::Feature kMediaRouterUIRouteController{
-    "MediaRouterUIRouteController", base::FEATURE_ENABLED_BY_DEFAULT};
 #endif  // !defined(OS_ANDROID)
 
 // Enables or disables modal permission prompts.
diff --git a/chrome/common/chrome_features.h b/chrome/common/chrome_features.h
index 2904b22..8bab90e 100644
--- a/chrome/common/chrome_features.h
+++ b/chrome/common/chrome_features.h
@@ -169,7 +169,6 @@
 
 #if !defined(OS_ANDROID)
 extern const base::Feature kMediaRemoting;
-extern const base::Feature kMediaRouterUIRouteController;
 #endif
 
 extern const base::Feature kModalPermissionPrompts;
diff --git a/chrome/test/data/webui/media_router/route_details_tests.js b/chrome/test/data/webui/media_router/route_details_tests.js
index 9ccd085..e376721 100644
--- a/chrome/test/data/webui/media_router/route_details_tests.js
+++ b/chrome/test/data/webui/media_router/route_details_tests.js
@@ -73,19 +73,6 @@
                        .hasAttribute('hidden'));
       };
 
-      // Checks the custom controller is shown.
-      var checkCustomControllerIsShown = function() {
-        assertTrue(details.$$('#route-description').hasAttribute('hidden'));
-        assertFalse(
-            details.$$('extension-view-wrapper').hasAttribute('hidden'));
-      };
-
-      // Checks whether |expected| and the text in the |elementId| element
-      // are equal given an id.
-      var checkElementTextWithId = function(expected, elementId) {
-        assertEquals(expected, details.$$('#' + elementId).innerText);
-      };
-
       // Import route_details.html before running suite.
       suiteSetup(function() {
         return PolymerTest.importHtml(
@@ -213,49 +200,6 @@
         checkDefaultViewIsShown();
         checkStartCastButtonIsShown();
       });
-
-      // Tests when |route| exists, has a custom controller, and it loads.
-      test('route has custom controller and loading succeeds', function(done) {
-        // Get the extension-view-wrapper stamped first, so that we can mock out
-        // the load method.
-        details.route = fakeRouteTwo;
-
-        setTimeout(function() {
-          details.$$('extension-view-wrapper').$$('#custom-controller').load =
-              function(url) {
-            setTimeout(function() {
-              assertEquals(
-                  fakeRouteOneControllerPath,
-                  url.substring(0, fakeRouteOneControllerPath.length));
-              checkCustomControllerIsShown();
-              done();
-            });
-            return Promise.resolve();
-          };
-
-          details.route = fakeRouteOne;
-        });
-      });
-
-      // Tests when |route| exists, has a custom controller, but fails to load.
-      test('route has custom controller but loading fails', function(done) {
-        // Get the extension-view-wrapper stamped first, so that we can mock out
-        // the load method.
-        details.route = fakeRouteTwo;
-
-        setTimeout(function() {
-          details.$$('extension-view-wrapper').$$('#custom-controller').load =
-              function(url) {
-            setTimeout(function() {
-              checkDefaultViewIsShown();
-              done();
-            });
-            return Promise.reject();
-          };
-
-          details.route = fakeRouteOne;
-        });
-      });
     });
   }
 
diff --git a/chromecast/media/cma/backend/fuchsia/cast_media_shlib_fuchsia.cc b/chromecast/media/cma/backend/fuchsia/cast_media_shlib_fuchsia.cc
index eb11b6e0b..cbf1e049 100644
--- a/chromecast/media/cma/backend/fuchsia/cast_media_shlib_fuchsia.cc
+++ b/chromecast/media/cma/backend/fuchsia/cast_media_shlib_fuchsia.cc
@@ -41,9 +41,8 @@
 }  // namespace
 
 void CastMediaShlib::Initialize(const std::vector<std::string>& argv) {
-  // Sets logging to display process and thread ID.
-  logging::SetLogItems(true, true, false, false);
-  chromecast::InitCommandLineShlib(argv);
+  // On Fuchsia CastMediaShlib is compiled statically with cast_shell, so |argv|
+  // can be ignored.
 
   g_video_plane = new DefaultVideoPlane();
 
diff --git a/components/chrome_cleaner/public/constants/constants.cc b/components/chrome_cleaner/public/constants/constants.cc
index 11ccb72..f20082ba 100644
--- a/components/chrome_cleaner/public/constants/constants.cc
+++ b/components/chrome_cleaner/public/constants/constants.cc
@@ -13,6 +13,7 @@
 const char kChromePromptSwitch[] = "chrome-prompt";
 const char kChromeSystemInstallSwitch[] = "chrome-system-install";
 const char kChromeVersionSwitch[] = "chrome-version";
+const char kWithScanningModeLogsSwitch[] = "with-scanning-mode-logs";
 const char kEnableCrashReportingSwitch[] = "enable-crash-reporting";
 const char kEngineSwitch[] = "engine";
 const char kExecutionModeSwitch[] = "execution-mode";
diff --git a/components/chrome_cleaner/public/constants/constants.h b/components/chrome_cleaner/public/constants/constants.h
index 4b96dd5..62c6280 100644
--- a/components/chrome_cleaner/public/constants/constants.h
+++ b/components/chrome_cleaner/public/constants/constants.h
@@ -33,6 +33,11 @@
 // The Chrome version string.
 extern const char kChromeVersionSwitch[];
 
+// Identify that the cleaner process in scanning mode is allowed to collect
+// logs. This should only be set if |kExecutionModeSwitch| is
+// ExecutionMode::kScanning.
+extern const char kWithScanningModeLogsSwitch[];
+
 // Indicates that crash reporting is enabled for the current user.
 extern const char kEnableCrashReportingSwitch[];
 
diff --git a/components/data_reduction_proxy/core/browser/data_reduction_proxy_config.cc b/components/data_reduction_proxy/core/browser/data_reduction_proxy_config.cc
index 54bb095..312d5bf 100644
--- a/components/data_reduction_proxy/core/browser/data_reduction_proxy_config.cc
+++ b/components/data_reduction_proxy/core/browser/data_reduction_proxy_config.cc
@@ -542,17 +542,7 @@
   warmup_url_fetcher_->FetchWarmupURL();
 }
 
-bool DataReductionProxyConfig::ShouldEnableLoFi(
-    const net::URLRequest& request,
-    const previews::PreviewsDecider& previews_decider) {
-  DCHECK(thread_checker_.CalledOnValidThread());
-  DCHECK((request.load_flags() & net::LOAD_MAIN_FRAME_DEPRECATED) != 0);
-  DCHECK(!request.url().SchemeIsCryptographic());
-
-  return ShouldAcceptServerPreview(request, previews_decider);
-}
-
-bool DataReductionProxyConfig::ShouldEnableLitePages(
+bool DataReductionProxyConfig::ShouldEnableServerPreviews(
     const net::URLRequest& request,
     const previews::PreviewsDecider& previews_decider) {
   DCHECK(thread_checker_.CalledOnValidThread());
diff --git a/components/data_reduction_proxy/core/browser/data_reduction_proxy_config.h b/components/data_reduction_proxy/core/browser/data_reduction_proxy_config.h
index abd2f3a..2a6d182 100644
--- a/components/data_reduction_proxy/core/browser/data_reduction_proxy_config.h
+++ b/components/data_reduction_proxy/core/browser/data_reduction_proxy_config.h
@@ -37,7 +37,7 @@
 class URLRequest;
 class URLRequestContextGetter;
 class URLRequestStatus;
-}
+}  // namespace net
 
 namespace previews {
 class PreviewsDecider;
@@ -173,18 +173,13 @@
   virtual bool ContainsDataReductionProxy(
       const net::ProxyConfig::ProxyRules& proxy_rules) const;
 
-  // Returns true when Lo-Fi Previews should be activated. Records metrics for
-  // Lo-Fi state changes. |request| is used to get the network quality estimator
-  // from the URLRequestContext. |previews_decider| is used to check if
-  // |request| is locally blacklisted.
-  bool ShouldEnableLoFi(const net::URLRequest& request,
-                        const previews::PreviewsDecider& previews_decider);
-
-  // Returns true when Lite Page Previews should be activated. |request| is used
-  // to get the network quality estimator from the URLRequestContext.
-  // |previews_decider| is used to check if |request| is locally blacklisted.
-  bool ShouldEnableLitePages(const net::URLRequest& request,
-                             const previews::PreviewsDecider& previews_decider);
+  // Returns true when server previews should be activated. Records metrics for
+  // previews state changes. |request| is used to get the network quality
+  // estimator from the URLRequestContext. |previews_decider| is used to check
+  // if |request| is locally blacklisted.
+  bool ShouldEnableServerPreviews(
+      const net::URLRequest& request,
+      const previews::PreviewsDecider& previews_decider);
 
   // Returns true if the data saver has been enabled by the user, and the data
   // saver proxy is reachable.
@@ -226,8 +221,7 @@
   friend class TestDataReductionProxyConfig;
   FRIEND_TEST_ALL_PREFIXES(DataReductionProxyConfigTest,
                            TestSetProxyConfigsHoldback);
-  FRIEND_TEST_ALL_PREFIXES(DataReductionProxyConfigTest,
-                           AreProxiesBypassed);
+  FRIEND_TEST_ALL_PREFIXES(DataReductionProxyConfigTest, AreProxiesBypassed);
   FRIEND_TEST_ALL_PREFIXES(DataReductionProxyConfigTest,
                            AreProxiesBypassedRetryDelay);
   FRIEND_TEST_ALL_PREFIXES(DataReductionProxyConfigTest, WarmupURL);
@@ -276,11 +270,10 @@
   // reduction proxies in min_retry_delay (if not NULL). If there are no
   // bypassed data reduction proxies for the request scheme, returns false and
   // does not assign min_retry_delay.
-  bool AreProxiesBypassed(
-      const net::ProxyRetryInfoMap& retry_map,
-      const net::ProxyConfig::ProxyRules& proxy_rules,
-      bool is_https,
-      base::TimeDelta* min_retry_delay) const;
+  bool AreProxiesBypassed(const net::ProxyRetryInfoMap& retry_map,
+                          const net::ProxyConfig::ProxyRules& proxy_rules,
+                          bool is_https,
+                          base::TimeDelta* min_retry_delay) const;
 
   // Returns whether the request is blacklisted (or if Lo-Fi is disabled).
   bool IsBlackListedOrDisabled(
diff --git a/components/data_reduction_proxy/core/browser/data_reduction_proxy_config_unittest.cc b/components/data_reduction_proxy/core/browser/data_reduction_proxy_config_unittest.cc
index 205b456..1b5f58a3 100644
--- a/components/data_reduction_proxy/core/browser/data_reduction_proxy_config_unittest.cc
+++ b/components/data_reduction_proxy/core/browser/data_reduction_proxy_config_unittest.cc
@@ -936,7 +936,7 @@
   }
 }
 
-TEST_F(DataReductionProxyConfigTest, ShouldEnableLoFi) {
+TEST_F(DataReductionProxyConfigTest, ShouldEnableServerPreviews) {
   base::test::ScopedFeatureList scoped_feature_list;
   scoped_feature_list.InitAndEnableFeature(
       features::kDataReductionProxyDecidesTransform);
@@ -949,33 +949,12 @@
                         net::LOAD_MAIN_FRAME_DEPRECATED);
   std::unique_ptr<TestPreviewsDecider> previews_decider =
       base::MakeUnique<TestPreviewsDecider>(true);
-  EXPECT_TRUE(
-      test_config()->ShouldEnableLoFi(*request.get(), *previews_decider.get()));
+  EXPECT_TRUE(test_config()->ShouldEnableServerPreviews(
+      *request.get(), *previews_decider.get()));
 
   previews_decider = base::MakeUnique<TestPreviewsDecider>(false);
-  EXPECT_FALSE(test_config()->ShouldEnableLitePages(*request.get(),
-                                                    *previews_decider.get()));
-}
-
-TEST_F(DataReductionProxyConfigTest, ShouldEnableLitePages) {
-  base::test::ScopedFeatureList scoped_feature_list;
-  scoped_feature_list.InitAndEnableFeature(
-      features::kDataReductionProxyDecidesTransform);
-
-  net::TestURLRequestContext context_;
-  net::TestDelegate delegate_;
-  std::unique_ptr<net::URLRequest> request = context_.CreateRequest(
-      GURL(), net::IDLE, &delegate_, TRAFFIC_ANNOTATION_FOR_TESTS);
-  request->SetLoadFlags(request->load_flags() |
-                        net::LOAD_MAIN_FRAME_DEPRECATED);
-  std::unique_ptr<TestPreviewsDecider> previews_decider =
-      base::MakeUnique<TestPreviewsDecider>(true);
-  EXPECT_TRUE(test_config()->ShouldEnableLitePages(*request.get(),
-                                                   *previews_decider.get()));
-
-  previews_decider = base::MakeUnique<TestPreviewsDecider>(false);
-  EXPECT_FALSE(test_config()->ShouldEnableLitePages(*request.get(),
-                                                    *previews_decider.get()));
+  EXPECT_FALSE(test_config()->ShouldEnableServerPreviews(
+      *request.get(), *previews_decider.get()));
 }
 
 TEST_F(DataReductionProxyConfigTest, ShouldAcceptServerPreview) {
diff --git a/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data.cc b/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data.cc
index 51fcbf3..b55c538 100644
--- a/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data.cc
+++ b/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data.cc
@@ -286,7 +286,7 @@
     config_client_->ApplySerializedConfig(serialized_config);
 }
 
-bool DataReductionProxyIOData::ShouldEnableLoFi(
+bool DataReductionProxyIOData::ShouldEnableServerPreviews(
     const net::URLRequest& request,
     previews::PreviewsDecider* previews_decider) {
   DCHECK(previews_decider);
@@ -295,19 +295,7 @@
                       request, configurator_->GetProxyConfig()))) {
     return false;
   }
-  return config_->ShouldEnableLoFi(request, *previews_decider);
-}
-
-bool DataReductionProxyIOData::ShouldEnableLitePages(
-    const net::URLRequest& request,
-    previews::PreviewsDecider* previews_decider) {
-  DCHECK(previews_decider);
-  DCHECK((request.load_flags() & net::LOAD_MAIN_FRAME_DEPRECATED) != 0);
-  if (!config_ || (config_->IsBypassedByDataReductionProxyLocalRules(
-                      request, configurator_->GetProxyConfig()))) {
-    return false;
-  }
-  return config_->ShouldEnableLitePages(request, *previews_decider);
+  return config_->ShouldEnableServerPreviews(request, *previews_decider);
 }
 
 void DataReductionProxyIOData::UpdateDataUseForHost(int64_t network_bytes,
diff --git a/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data.h b/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data.h
index e5e1aaae..8a1e32a 100644
--- a/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data.h
+++ b/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data.h
@@ -101,21 +101,12 @@
   // Applies a serialized Data Reduction Proxy configuration.
   void SetDataReductionProxyConfiguration(const std::string& serialized_config);
 
-  // Returns true when Lo-Fi Previews should be activated. When Lo-Fi is
-  // active, URL requests are modified to request low fidelity versions of the
-  // resources, except when the user is in the Lo-Fi control group.
-  // |previews_decider| is a non-null object that determines eligibility of
-  // showing the preview based on past opt outs.
-  bool ShouldEnableLoFi(const net::URLRequest& request,
-                        previews::PreviewsDecider* previews_decider);
-
-  // Returns true when Lite Page Previews should be activated. When Lite Pages
-  // are active, a low fidelity transcoded page is requested on the main frame
-  // resource, except when the user is in the control group. |previews_decider|
-  // is a non-null object that determines eligibility of showing the preview
-  // based on past opt outs.
-  bool ShouldEnableLitePages(const net::URLRequest& request,
-                             previews::PreviewsDecider* previews_decider);
+  // Returns true when server previews should be activated. When server previews
+  // are active, URL requests are modified to request low fidelity versions of
+  // the resources.|previews_decider| is a non-null object that determines
+  // eligibility of showing the preview based on past opt outs.
+  bool ShouldEnableServerPreviews(const net::URLRequest& request,
+                                  previews::PreviewsDecider* previews_decider);
 
   // Bridge methods to safely call to the UI thread objects.
   void UpdateDataUseForHost(int64_t network_bytes,
diff --git a/components/data_reduction_proxy/core/browser/data_reduction_proxy_network_delegate_unittest.cc b/components/data_reduction_proxy/core/browser/data_reduction_proxy_network_delegate_unittest.cc
index 528cdff..8d83f28 100644
--- a/components/data_reduction_proxy/core/browser/data_reduction_proxy_network_delegate_unittest.cc
+++ b/components/data_reduction_proxy/core/browser/data_reduction_proxy_network_delegate_unittest.cc
@@ -981,7 +981,7 @@
       std::unique_ptr<net::URLRequest> fake_request = context()->CreateRequest(
           GURL(kTestURL), net::IDLE, &delegate, TRAFFIC_ANNOTATION_FOR_TESTS);
       fake_request->SetLoadFlags(net::LOAD_MAIN_FRAME_DEPRECATED);
-      lofi_decider()->SetIsUsingLoFi(config()->ShouldEnableLoFi(
+      lofi_decider()->SetIsUsingLoFi(config()->ShouldEnableServerPreviews(
           *fake_request.get(), test_previews_decider));
       NotifyNetworkDelegate(fake_request.get(), data_reduction_proxy_info,
                             proxy_retry_info, &headers);
@@ -989,8 +989,8 @@
       VerifyHeaders(tests[i].is_data_reduction_proxy, true, headers);
       VerifyDataReductionProxyData(
           *fake_request, tests[i].is_data_reduction_proxy,
-          config()->ShouldEnableLoFi(*fake_request.get(),
-                                     test_previews_decider));
+          config()->ShouldEnableServerPreviews(*fake_request.get(),
+                                               test_previews_decider));
     }
 
     {
@@ -1064,14 +1064,14 @@
       std::unique_ptr<net::URLRequest> fake_request = context()->CreateRequest(
           GURL(kTestURL), net::IDLE, &delegate, TRAFFIC_ANNOTATION_FOR_TESTS);
       fake_request->SetLoadFlags(net::LOAD_MAIN_FRAME_DEPRECATED);
-      lofi_decider()->SetIsUsingLoFi(config()->ShouldEnableLoFi(
+      lofi_decider()->SetIsUsingLoFi(config()->ShouldEnableServerPreviews(
           *fake_request.get(), test_previews_decider));
       NotifyNetworkDelegate(fake_request.get(), data_reduction_proxy_info,
                             proxy_retry_info, &headers);
       VerifyDataReductionProxyData(
           *fake_request, tests[i].is_data_reduction_proxy,
-          config()->ShouldEnableLoFi(*fake_request.get(),
-                                     test_previews_decider));
+          config()->ShouldEnableServerPreviews(*fake_request.get(),
+                                               test_previews_decider));
     }
   }
 }
@@ -1361,8 +1361,8 @@
 
     // Needed as a parameter, but functionality is not tested.
     TestPreviewsDecider test_previews_decider;
-    lofi_decider()->SetIsUsingLoFi(
-        config()->ShouldEnableLoFi(*fake_request.get(), test_previews_decider));
+    lofi_decider()->SetIsUsingLoFi(config()->ShouldEnableServerPreviews(
+        *fake_request.get(), test_previews_decider));
 
     fake_request = (FetchURLRequest(GURL(kTestURL), nullptr, response_headers,
                                     kResponseContentLength, 0));
diff --git a/components/os_crypt/key_storage_config_linux.h b/components/os_crypt/key_storage_config_linux.h
index 6199cbd..78cbcf8 100644
--- a/components/os_crypt/key_storage_config_linux.h
+++ b/components/os_crypt/key_storage_config_linux.h
@@ -33,6 +33,9 @@
   bool should_use_preference;
   // Preferences are stored in a separate file in the user data directory.
   base::FilePath user_data_path;
+  // Communication with the backend via dbus needs to happen on a specific
+  // thread. Currently, only KWallet needs to use dbus.
+  scoped_refptr<base::SequencedTaskRunner> dbus_task_runner;
 
  private:
   DISALLOW_COPY_AND_ASSIGN(Config);
diff --git a/components/os_crypt/key_storage_kwallet.cc b/components/os_crypt/key_storage_kwallet.cc
index 53f00c1..ae029a2 100644
--- a/components/os_crypt/key_storage_kwallet.cc
+++ b/components/os_crypt/key_storage_kwallet.cc
@@ -8,12 +8,18 @@
 
 #include "base/base64.h"
 #include "base/rand_util.h"
+#include "base/sequenced_task_runner.h"
 #include "components/os_crypt/kwallet_dbus.h"
 #include "dbus/bus.h"
 
-KeyStorageKWallet::KeyStorageKWallet(base::nix::DesktopEnvironment desktop_env,
-                                     std::string app_name)
-    : desktop_env_(desktop_env), handle_(-1), app_name_(std::move(app_name)) {}
+KeyStorageKWallet::KeyStorageKWallet(
+    base::nix::DesktopEnvironment desktop_env,
+    std::string app_name,
+    scoped_refptr<base::SequencedTaskRunner> dbus_task_runner)
+    : desktop_env_(desktop_env),
+      handle_(-1),
+      app_name_(std::move(app_name)),
+      dbus_task_runner_(dbus_task_runner) {}
 
 KeyStorageKWallet::~KeyStorageKWallet() {
   // The handle is shared between programs that are using the same wallet.
@@ -23,7 +29,12 @@
   kwallet_dbus_->GetSessionBus()->ShutdownAndBlock();
 }
 
+base::SequencedTaskRunner* KeyStorageKWallet::GetTaskRunner() {
+  return dbus_task_runner_.get();
+}
+
 bool KeyStorageKWallet::Init() {
+  DCHECK(dbus_task_runner_->RunsTasksInCurrentSequence());
   // Initialize using the production KWalletDBus.
   return InitWithKWalletDBus(nullptr);
 }
@@ -80,6 +91,8 @@
 }
 
 std::string KeyStorageKWallet::GetKeyImpl() {
+  DCHECK(dbus_task_runner_->RunsTasksInCurrentSequence());
+
   // Get handle
   KWalletDBus::Error error =
       kwallet_dbus_->Open(wallet_name_, app_name_, &handle_);
diff --git a/components/os_crypt/key_storage_kwallet.h b/components/os_crypt/key_storage_kwallet.h
index 209b2fa..d747f3f 100644
--- a/components/os_crypt/key_storage_kwallet.h
+++ b/components/os_crypt/key_storage_kwallet.h
@@ -12,10 +12,15 @@
 #include "components/os_crypt/key_storage_linux.h"
 #include "components/os_crypt/kwallet_dbus.h"
 
+namespace base {
+class SequencedTaskRunner;
+}
+
 class KeyStorageKWallet : public KeyStorageLinux {
  public:
   KeyStorageKWallet(base::nix::DesktopEnvironment desktop_env,
-                    std::string app_name);
+                    std::string app_name,
+                    scoped_refptr<base::SequencedTaskRunner> dbus_task_runner);
   ~KeyStorageKWallet() override;
 
   // Initialize using an optional KWalletDBus mock.
@@ -25,6 +30,7 @@
 
  protected:
   // KeyStorageLinux
+  base::SequencedTaskRunner* GetTaskRunner() override;
   bool Init() override;
   std::string GetKeyImpl() override;
 
@@ -46,6 +52,7 @@
   std::string wallet_name_;
   const std::string app_name_;
   std::unique_ptr<KWalletDBus> kwallet_dbus_;
+  scoped_refptr<base::SequencedTaskRunner> dbus_task_runner_;
 
   DISALLOW_COPY_AND_ASSIGN(KeyStorageKWallet);
 };
diff --git a/components/os_crypt/key_storage_kwallet_unittest.cc b/components/os_crypt/key_storage_kwallet_unittest.cc
index 9f9b7da..68ff9698 100644
--- a/components/os_crypt/key_storage_kwallet_unittest.cc
+++ b/components/os_crypt/key_storage_kwallet_unittest.cc
@@ -6,6 +6,7 @@
 
 #include "base/logging.h"
 #include "base/nix/xdg_util.h"
+#include "base/test/test_simple_task_runner.h"
 #include "dbus/message.h"
 #include "dbus/mock_bus.h"
 #include "dbus/mock_object_proxy.h"
@@ -95,7 +96,9 @@
 
 class KeyStorageKWalletTest : public testing::Test {
  public:
-  KeyStorageKWalletTest() : key_storage_kwallet_(kDesktopEnv, "test-app") {}
+  KeyStorageKWalletTest()
+      : task_runner_(base::MakeRefCounted<base::TestSimpleTaskRunner>()),
+        key_storage_kwallet_(kDesktopEnv, "test-app", task_runner_) {}
 
   void SetUp() override {
     kwallet_dbus_mock_ = new StrictMock<MockKWalletDBus>();
@@ -120,6 +123,7 @@
 
  protected:
   StrictMock<MockKWalletDBus>* kwallet_dbus_mock_;
+  scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
   KeyStorageKWallet key_storage_kwallet_;
   const std::string wallet_name_ = "mollet";
 
@@ -230,7 +234,8 @@
     : public testing::TestWithParam<KWalletDBus::Error> {
  public:
   KeyStorageKWalletFailuresTest()
-      : key_storage_kwallet_(kDesktopEnv, "test-app") {}
+      : task_runner_(new base::TestSimpleTaskRunner()),
+        key_storage_kwallet_(kDesktopEnv, "test-app", task_runner_) {}
 
   void SetUp() override {
     // |key_storage_kwallet_| will take ownership of |kwallet_dbus_mock_|.
@@ -255,6 +260,7 @@
 
  protected:
   StrictMock<MockKWalletDBus>* kwallet_dbus_mock_;
+  scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
   KeyStorageKWallet key_storage_kwallet_;
   const std::string wallet_name_ = "mollet";
 
diff --git a/components/os_crypt/key_storage_linux.cc b/components/os_crypt/key_storage_linux.cc
index 3be8806..3a04ce5 100644
--- a/components/os_crypt/key_storage_linux.cc
+++ b/components/os_crypt/key_storage_linux.cc
@@ -80,7 +80,7 @@
 #if defined(USE_LIBSECRET)
   if (selected_backend == os_crypt::SelectedLinuxBackend::GNOME_ANY ||
       selected_backend == os_crypt::SelectedLinuxBackend::GNOME_LIBSECRET) {
-    key_storage.reset(new KeyStorageLibsecret());
+    key_storage = std::make_unique<KeyStorageLibsecret>();
     if (key_storage->WaitForInitOnTaskRunner()) {
       VLOG(1) << "OSCrypt using Libsecret as backend.";
       return key_storage;
@@ -91,7 +91,8 @@
 #if defined(USE_KEYRING)
   if (selected_backend == os_crypt::SelectedLinuxBackend::GNOME_ANY ||
       selected_backend == os_crypt::SelectedLinuxBackend::GNOME_KEYRING) {
-    key_storage.reset(new KeyStorageKeyring(config.main_thread_runner));
+    key_storage =
+        std::make_unique<KeyStorageKeyring>(config.main_thread_runner);
     if (key_storage->WaitForInitOnTaskRunner()) {
       VLOG(1) << "OSCrypt using Keyring as backend.";
       return key_storage;
@@ -107,8 +108,8 @@
         selected_backend == os_crypt::SelectedLinuxBackend::KWALLET
             ? base::nix::DESKTOP_ENVIRONMENT_KDE4
             : base::nix::DESKTOP_ENVIRONMENT_KDE5;
-    key_storage.reset(
-        new KeyStorageKWallet(used_desktop_env, config.product_name));
+    key_storage = std::make_unique<KeyStorageKWallet>(
+        used_desktop_env, config.product_name, config.dbus_task_runner);
     if (key_storage->WaitForInitOnTaskRunner()) {
       VLOG(1) << "OSCrypt using KWallet as backend.";
       return key_storage;
diff --git a/content/common/content_switches_internal.cc b/content/common/content_switches_internal.cc
index 1033dd0..5e8ebae 100644
--- a/content/common/content_switches_internal.cc
+++ b/content/common/content_switches_internal.cc
@@ -9,6 +9,7 @@
 #include "base/command_line.h"
 #include "base/feature_list.h"
 #include "base/metrics/field_trial.h"
+#include "base/metrics/field_trial_params.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
@@ -56,6 +57,9 @@
     base::FEATURE_DISABLED_BY_DEFAULT};
 #endif
 
+const base::Feature kSavePreviousDocumentResources{
+    "SavePreviousDocumentResources", base::FEATURE_DISABLED_BY_DEFAULT};
+
 #if defined(OS_WIN)
 
 base::string16 ToNativeString(base::StringPiece string) {
@@ -157,6 +161,14 @@
     return SavePreviousDocumentResources::UNTIL_ON_DOM_CONTENT_LOADED;
   if (save_previous_document_resources == "onload")
     return SavePreviousDocumentResources::UNTIL_ON_LOAD;
+  // The command line, which is set by the user, takes priority. Otherwise,
+  // fall back to the field trial.
+  std::string until = base::GetFieldTrialParamValueByFeature(
+      kSavePreviousDocumentResources, "until");
+  if (until == "onDOMContentLoaded")
+    return SavePreviousDocumentResources::UNTIL_ON_DOM_CONTENT_LOADED;
+  if (until == "onload")
+    return SavePreviousDocumentResources::UNTIL_ON_LOAD;
   return SavePreviousDocumentResources::NEVER;
 }
 
diff --git a/content/public/android/java/src/org/chromium/content/common/ContentSwitches.java b/content/public/android/java/src/org/chromium/content/common/ContentSwitches.java
index 3d92d19f..81b6863 100644
--- a/content/public/android/java/src/org/chromium/content/common/ContentSwitches.java
+++ b/content/public/android/java/src/org/chromium/content/common/ContentSwitches.java
@@ -36,9 +36,6 @@
     // How much of the browser controls need to be hidden before they will auto hide.
     public static final String TOP_CONTROLS_HIDE_THRESHOLD = "top-controls-hide-threshold";
 
-    // Native switch - shell_switches::kRunLayoutTest
-    public static final String RUN_LAYOUT_TEST = "run-layout-test";
-
     // Native switch - chrome_switches::kDisablePopupBlocking
     public static final String DISABLE_POPUP_BLOCKING = "disable-popup-blocking";
 
diff --git a/content/public/app/mojo/content_browser_manifest.json b/content/public/app/mojo/content_browser_manifest.json
index 6e5582fe..8311a052 100644
--- a/content/public/app/mojo/content_browser_manifest.json
+++ b/content/public/app/mojo/content_browser_manifest.json
@@ -24,7 +24,6 @@
           "blink::mojom::BroadcastChannelProvider",
           "blink::mojom::Hyphenation",
           "blink::mojom::MimeRegistry",
-          "blink::mojom::NotificationService",
           "blink::mojom::OffscreenCanvasProvider",
           "blink::mojom::ReportingServiceProxy",
           "blink::mojom::WebDatabaseHost",
diff --git a/content/renderer/media/aec_dump_message_filter.cc b/content/renderer/media/aec_dump_message_filter.cc
index 56f40ee..f423391 100644
--- a/content/renderer/media/aec_dump_message_filter.cc
+++ b/content/renderer/media/aec_dump_message_filter.cc
@@ -48,10 +48,6 @@
   int id = delegate_id_counter_++;
   delegates_[id] = delegate;
 
-  if (override_aec3_) {
-    delegate->OnAec3Enable(*override_aec3_);
-  }
-
   io_task_runner_->PostTask(
       FROM_HERE,
       base::BindOnce(&AecDumpMessageFilter::RegisterAecDumpConsumer, this, id));
@@ -72,6 +68,11 @@
                      id));
 }
 
+base::Optional<bool> AecDumpMessageFilter::GetOverrideAec3() const {
+  DCHECK(main_task_runner_->BelongsToCurrentThread());
+  return override_aec3_;
+}
+
 void AecDumpMessageFilter::Send(IPC::Message* message) {
   DCHECK(io_task_runner_->BelongsToCurrentThread());
   if (sender_)
diff --git a/content/renderer/media/aec_dump_message_filter.h b/content/renderer/media/aec_dump_message_filter.h
index 7df6635..3e64108 100644
--- a/content/renderer/media/aec_dump_message_filter.h
+++ b/content/renderer/media/aec_dump_message_filter.h
@@ -8,6 +8,7 @@
 #include <memory>
 
 #include "base/macros.h"
+#include "base/optional.h"
 #include "content/common/content_export.h"
 #include "content/renderer/render_thread_impl.h"
 #include "ipc/ipc_platform_file.h"
@@ -40,10 +41,13 @@
   // Getter for the one AecDumpMessageFilter object.
   static scoped_refptr<AecDumpMessageFilter> Get();
 
-  // Adds a delegate that receives the enable and disable notifications.
+  // Adds a delegate that receives the enable and disable notifications. Must be
+  // called on the main task runner (|main_task_runner| in constructor). All
+  // calls on |delegate| are done on the main task runner.
   void AddDelegate(AecDumpMessageFilter::AecDumpDelegate* delegate);
 
-  // Removes a delegate.
+  // Removes a delegate. Must be called on the main task runner
+  // (|main_task_runner| in constructor).
   void RemoveDelegate(AecDumpMessageFilter::AecDumpDelegate* delegate);
 
   // IO task runner associated with this message filter.
@@ -51,6 +55,10 @@
     return io_task_runner_;
   }
 
+  // Returns the AEC3 setting. Must be called on the main task runner
+  // (|main_task_runner| in constructor).
+  base::Optional<bool> GetOverrideAec3() const;
+
  protected:
   ~AecDumpMessageFilter() override;
 
diff --git a/content/renderer/media/media_stream_audio_processor.cc b/content/renderer/media/media_stream_audio_processor.cc
index d314246..b874972 100644
--- a/content/renderer/media/media_stream_audio_processor.cc
+++ b/content/renderer/media/media_stream_audio_processor.cc
@@ -320,12 +320,17 @@
   DCHECK(main_thread_runner_);
   capture_thread_checker_.DetachFromThread();
   render_thread_checker_.DetachFromThread();
+
+  // In unit tests not creating a message filter, |aec_dump_message_filter_|
+  // will be null. We can just ignore that below. Other unit tests and browser
+  // tests ensure that we do get the filter when we should.
+  aec_dump_message_filter_ = AecDumpMessageFilter::Get();
+
+  if (aec_dump_message_filter_)
+    override_aec3_ = aec_dump_message_filter_->GetOverrideAec3();
+
   InitializeAudioProcessingModule(properties);
 
-  aec_dump_message_filter_ = AecDumpMessageFilter::Get();
-  // In unit tests not creating a message filter, |aec_dump_message_filter_|
-  // will be NULL. We can just ignore that. Other unit tests and browser tests
-  // ensure that we do get the filter when we should.
   if (aec_dump_message_filter_.get())
     aec_dump_message_filter_->AddDelegate(this);
 
diff --git a/content/shell/android/shell_apk/src/org/chromium/content_shell_apk/ContentShellActivity.java b/content/shell/android/shell_apk/src/org/chromium/content_shell_apk/ContentShellActivity.java
index 60a4195..85e8964 100644
--- a/content/shell/android/shell_apk/src/org/chromium/content_shell_apk/ContentShellActivity.java
+++ b/content/shell/android/shell_apk/src/org/chromium/content_shell_apk/ContentShellActivity.java
@@ -21,7 +21,6 @@
 import org.chromium.content.browser.BrowserStartupController;
 import org.chromium.content.browser.ContentViewCore;
 import org.chromium.content.browser.DeviceUtils;
-import org.chromium.content.common.ContentSwitches;
 import org.chromium.content_public.browser.WebContents;
 import org.chromium.content_shell.Shell;
 import org.chromium.content_shell.ShellManager;
@@ -37,6 +36,9 @@
     private static final String ACTIVE_SHELL_URL_KEY = "activeUrl";
     public static final String COMMAND_LINE_ARGS_KEY = "commandLineArgs";
 
+    // Native switch - shell_switches::kRunLayoutTest
+    private static final String RUN_LAYOUT_TEST_SWITCH = "run-layout-test";
+
     private ShellManager mShellManager;
     private ActivityWindowAndroid mWindowAndroid;
     private Intent mLastSentIntent;
@@ -83,7 +85,7 @@
             mShellManager.setStartupUrl(Shell.sanitizeUrl(mStartupUrl));
         }
 
-        if (CommandLine.getInstance().hasSwitch(ContentSwitches.RUN_LAYOUT_TEST)) {
+        if (CommandLine.getInstance().hasSwitch(RUN_LAYOUT_TEST_SWITCH)) {
             try {
                 BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER)
                         .startBrowserProcessesSync(false);
diff --git a/content/test/gpu/gpu_tests/webgl2_conformance_expectations.py b/content/test/gpu/gpu_tests/webgl2_conformance_expectations.py
index 2dcffbb..a72825a 100644
--- a/content/test/gpu/gpu_tests/webgl2_conformance_expectations.py
+++ b/content/test/gpu/gpu_tests/webgl2_conformance_expectations.py
@@ -295,6 +295,10 @@
     self.Fail('deqp/functional/gles3/integerstatequery.html',
         ['passthrough', 'opengl'], bug=602688)
 
+    # Win / Intel
+    self.Fail('conformance/rendering/rendering-stencil-large-viewport.html',
+        ['win', 'intel', 'd3d11'], bug=782317)
+
     # Passthrough command decoder / OpenGL / Intel
     self.Fail('conformance2/textures/video/tex-2d-rgb32f-rgb-float.html',
         ['passthrough', 'opengl', 'intel'], bug=602688)
@@ -696,6 +700,9 @@
         'multisampled-depth-renderbuffer-initialization.html',
         ['mac', 'intel'], bug=731877)
 
+    self.Fail('conformance/rendering/rendering-stencil-large-viewport.html',
+        ['mac', 'intel'], bug=782317)
+
     # Linux only.
     self.Flaky('conformance/textures/video/' +
                'tex-2d-rgba-rgba-unsigned_byte.html',
diff --git a/content/test/gpu/gpu_tests/webgl_conformance_revision.txt b/content/test/gpu/gpu_tests/webgl_conformance_revision.txt
index d770d70b..76eabf3 100644
--- a/content/test/gpu/gpu_tests/webgl_conformance_revision.txt
+++ b/content/test/gpu/gpu_tests/webgl_conformance_revision.txt
@@ -1,3 +1,3 @@
 # AUTOGENERATED FILE - DO NOT EDIT
 # SEE roll_webgl_conformance.py
-Current webgl revision e4919fa03c74bd561dcabf3e61668fa3c7e54353
+Current webgl revision 05591bbeae6592fd924caec8e728a4ea86cbb8c9
diff --git a/ios/chrome/app/main_controller.mm b/ios/chrome/app/main_controller.mm
index cb6f7b7..fc999c3ff 100644
--- a/ios/chrome/app/main_controller.mm
+++ b/ios/chrome/app/main_controller.mm
@@ -1873,14 +1873,18 @@
     if (![self.mainTabModel isEmpty] || ![self.otrTabModel isEmpty])
       return NO;
 
-    UIViewController* viewController = [self topPresentedViewController];
-    while (viewController) {
-      if ([viewController.presentingViewController
-              isEqual:_tabSwitcherController]) {
-        return NO;
-      }
-      viewController = viewController.presentingViewController;
+    // If the tabSwitcher is contained, check if the parent container is
+    // presenting another view controller.
+    if ([[_tabSwitcherController parentViewController]
+            presentedViewController]) {
+      return NO;
     }
+
+    // Check if the tabSwitcher is directly presenting another view controller.
+    if ([_tabSwitcherController presentedViewController]) {
+      return NO;
+    }
+
     return YES;
   }
   return ![tabModel count] && [tabModel browserState] &&
diff --git a/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.h b/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.h
index 4ebc3aa..5db36bd 100644
--- a/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.h
+++ b/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.h
@@ -12,9 +12,6 @@
  public:
   virtual ~ChromeBroadcastObserverInterface();
 
-  // Invoked by |-broadcastTabStripVisible:|.
-  virtual void OnTabStripVisbibleBroadcasted(bool visible) {}
-
   // Invoked by |-broadcastContentScrollOffset:|.
   virtual void OnContentScrollOffsetBroadcasted(CGFloat offset) {}
 
diff --git a/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.mm b/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.mm
index a9f34e3..6c4e3da 100644
--- a/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.mm
+++ b/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.mm
@@ -23,10 +23,6 @@
   return self;
 }
 
-- (void)broadcastTabStripVisible:(BOOL)visible {
-  self.observer->OnTabStripVisbibleBroadcasted(visible);
-}
-
 - (void)broadcastContentScrollOffset:(CGFloat)offset {
   self.observer->OnContentScrollOffsetBroadcasted(offset);
 }
diff --git a/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge_unittest.mm b/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge_unittest.mm
index 3c640e3..85dd2641 100644
--- a/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge_unittest.mm
+++ b/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge_unittest.mm
@@ -14,7 +14,6 @@
 class TestChromeBroadcastObserver : public ChromeBroadcastObserverInterface {
  public:
   // Received broadcast values.
-  bool tab_strip_visible() const { return tab_strip_visible_; }
   CGFloat scroll_offset() const { return scroll_offset_; }
   bool scroll_view_scrolling() const { return scroll_view_scrolling_; }
   bool scroll_view_dragging() const { return scroll_view_dragging_; }
@@ -22,9 +21,6 @@
 
  private:
   // ChromeBroadcastObserverInterface:
-  void OnTabStripVisbibleBroadcasted(bool visible) override {
-    tab_strip_visible_ = visible;
-  }
   void OnContentScrollOffsetBroadcasted(CGFloat offset) override {
     scroll_offset_ = offset;
   }
@@ -38,7 +34,6 @@
     toolbar_height_ = toolbar_height;
   }
 
-  bool tab_strip_visible_ = false;
   CGFloat scroll_offset_ = 0.0;
   bool scroll_view_scrolling_ = false;
   bool scroll_view_dragging_ = false;
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_controller.h b/ios/chrome/browser/ui/fullscreen/fullscreen_controller.h
index fefb9f7..4b4f6e24 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_controller.h
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_controller.h
@@ -5,10 +5,13 @@
 #ifndef IOS_CLEAN_CHROME_BROWSER_UI_FULLSCREEN_FULLSCREEN_CONTROLLER_H_
 #define IOS_CLEAN_CHROME_BROWSER_UI_FULLSCREEN_FULLSCREEN_CONTROLLER_H_
 
+#import <Foundation/Foundation.h>
 #include <memory>
 
 #include "base/macros.h"
 
+@class ChromeBroadcaster;
+@class ChromeBroadcastOberverBridge;
 class FullscreenControllerObserver;
 class FullscreenModel;
 class FullscreenWebStateListObserver;
@@ -19,7 +22,7 @@
 // the page's content to be visible.
 class FullscreenController {
  public:
-  explicit FullscreenController();
+  explicit FullscreenController(ChromeBroadcaster* broadcaster);
   ~FullscreenController();
 
   // Adds and removes FullscreenControllerObservers.
@@ -39,8 +42,12 @@
   void DecrementDisabledCounter();
 
  private:
+  // The broadcaster that drives the model.
+  __strong ChromeBroadcaster* broadcaster_ = nil;
   // The model used to calculate fullscreen state.
   std::unique_ptr<FullscreenModel> model_;
+  // The bridge used to forward brodcasted UI to |model_|.
+  __strong ChromeBroadcastOberverBridge* bridge_ = nil;
   // A WebStateListObserver that updates |model_| for WebStateList changes.
   std::unique_ptr<FullscreenWebStateListObserver> web_state_list_observer_;
 
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_controller.mm b/ios/chrome/browser/ui/fullscreen/fullscreen_controller.mm
index 7754bec1..c496895 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_controller.mm
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_controller.mm
@@ -5,6 +5,8 @@
 #import "ios/chrome/browser/ui/fullscreen/fullscreen_controller.h"
 
 #include "base/memory/ptr_util.h"
+#import "ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.h"
+#import "ios/chrome/browser/ui/broadcaster/chrome_broadcaster.h"
 #import "ios/chrome/browser/ui/fullscreen/fullscreen_model.h"
 #import "ios/chrome/browser/ui/fullscreen/fullscreen_web_state_list_observer.h"
 
@@ -12,10 +14,32 @@
 #error "This file requires ARC support."
 #endif
 
-FullscreenController::FullscreenController()
-    : model_(base::MakeUnique<FullscreenModel>()) {}
+FullscreenController::FullscreenController(ChromeBroadcaster* broadcaster)
+    : broadcaster_(broadcaster),
+      model_(base::MakeUnique<FullscreenModel>()),
+      bridge_([[ChromeBroadcastOberverBridge alloc]
+          initWithObserver:model_.get()]) {
+  DCHECK(broadcaster_);
+  [broadcaster_ addObserver:bridge_
+                forSelector:@selector(broadcastContentScrollOffset:)];
+  [broadcaster_ addObserver:bridge_
+                forSelector:@selector(broadcastScrollViewIsScrolling:)];
+  [broadcaster_ addObserver:bridge_
+                forSelector:@selector(broadcastScrollViewIsDragging:)];
+  [broadcaster_ addObserver:bridge_
+                forSelector:@selector(broadcastToolbarHeight:)];
+}
 
-FullscreenController::~FullscreenController() {}
+FullscreenController::~FullscreenController() {
+  [broadcaster_ removeObserver:bridge_
+                   forSelector:@selector(broadcastContentScrollOffset:)];
+  [broadcaster_ removeObserver:bridge_
+                   forSelector:@selector(broadcastScrollViewIsScrolling:)];
+  [broadcaster_ removeObserver:bridge_
+                   forSelector:@selector(broadcastScrollViewIsDragging:)];
+  [broadcaster_ removeObserver:bridge_
+                   forSelector:@selector(broadcastToolbarHeight:)];
+}
 
 void FullscreenController::AddObserver(FullscreenControllerObserver* observer) {
   // TODO(crbug.com/785671): Use FullscreenControllerObserverManager to keep
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_model.h b/ios/chrome/browser/ui/fullscreen/fullscreen_model.h
index 8ce61031..70df003 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_model.h
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_model.h
@@ -10,14 +10,15 @@
 
 #include "base/macros.h"
 #include "base/observer_list.h"
+#import "ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.h"
 
 class FullscreenModelObserver;
 
 // Model object used to calculate fullscreen state.
-class FullscreenModel {
+class FullscreenModel : public ChromeBroadcastObserverInterface {
  public:
   FullscreenModel();
-  virtual ~FullscreenModel();
+  ~FullscreenModel() override;
 
   // Adds and removes FullscreenModelObservers.
   void AddObserver(FullscreenModelObserver* observer) {
@@ -88,6 +89,12 @@
   // toolbar height.
   void UpdateBaseOffset();
 
+  // ChromeBroadcastObserverInterface:
+  void OnContentScrollOffsetBroadcasted(CGFloat offset) override;
+  void OnScrollViewIsScrollingBroadcasted(bool scrolling) override;
+  void OnScrollViewIsDraggingBroadcasted(bool dragging) override;
+  void OnToolbarHeightBroadcasted(CGFloat toolbar_height) override;
+
   // The observers for this model.
   base::ObserverList<FullscreenModelObserver> observers_;
   // The percentage of the toolbar that should be visible, where 1.0 denotes a
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_model.mm b/ios/chrome/browser/ui/fullscreen/fullscreen_model.mm
index e28e10d..4f0b885 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_model.mm
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_model.mm
@@ -126,3 +126,19 @@
 void FullscreenModel::UpdateBaseOffset() {
   base_offset_ = y_content_offset_ - (1.0 - progress_) * toolbar_height_;
 }
+
+void FullscreenModel::OnContentScrollOffsetBroadcasted(CGFloat offset) {
+  SetYContentOffset(offset);
+}
+
+void FullscreenModel::OnScrollViewIsScrollingBroadcasted(bool scrolling) {
+  SetScrollViewIsScrolling(scrolling);
+}
+
+void FullscreenModel::OnScrollViewIsDraggingBroadcasted(bool dragging) {
+  SetScrollViewIsDragging(dragging);
+}
+
+void FullscreenModel::OnToolbarHeightBroadcasted(CGFloat toolbar_height) {
+  SetToolbarHeight(toolbar_height);
+}
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_scroll_end_animator.h b/ios/chrome/browser/ui/fullscreen/fullscreen_scroll_end_animator.h
index ca15252..c16d3669 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_scroll_end_animator.h
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_scroll_end_animator.h
@@ -7,7 +7,6 @@
 
 #import <UIKit/UIKit.h>
 
-#if defined(__IPHONE_10_0) && (__IPHONE_OS_VERSION_MIN_ALLOWED >= __IPHONE_10_0)
 // When a scroll event ends, the toolbar should be either completely hidden or
 // completely visible.  If a scroll ends and the toolbar is partly visible, this
 // animator will be provided to UI elements to animate its state to a hidden or
@@ -29,17 +28,8 @@
 - (instancetype)initWithDuration:(NSTimeInterval)duration
                 timingParameters:(id<UITimingCurveProvider>)parameters
     NS_UNAVAILABLE;
+- (instancetype)init NS_UNAVAILABLE;
 
 @end
 
-#else
-
-// Dummy object.
-// TODO(crbug.com/768876): Remove this class and the #if guards once iOS9
-// support is dropped.
-@interface FullscreenScrollEndAnimator : NSObject
-@end
-
-#endif  // __IPHONE_10_0
-
 #endif  // IOS_CLEAN_CHROME_BROWSER_UI_FULLSCREEN_FULLSCREEN_SCROLL_END_ANIMATOR_H_
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_scroll_end_animator.mm b/ios/chrome/browser/ui/fullscreen/fullscreen_scroll_end_animator.mm
index 12604fd0..a9cbdaee 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_scroll_end_animator.mm
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_scroll_end_animator.mm
@@ -16,7 +16,6 @@
 #error "This file requires ARC support."
 #endif
 
-#if defined(__IPHONE_10_0) && (__IPHONE_OS_VERSION_MIN_ALLOWED >= __IPHONE_10_0)
 @interface FullscreenScrollEndTimingCurveProvider
     : NSObject<UITimingCurveProvider> {
   std::unique_ptr<gfx::CubicBezier> _bezier;
@@ -114,10 +113,3 @@
 }
 
 @end
-
-#else
-
-@implementation FullscreenScrollEndAnimator
-@end
-
-#endif  // __IPHONE_10_0
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater_unittest.mm b/ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater_unittest.mm
index 0b9e6fc8..0a7a9317 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater_unittest.mm
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater_unittest.mm
@@ -80,8 +80,10 @@
 // Tests that the updater sends the animator to the UI element.
 TEST_F(FullscreenUIUpdaterTest, ScrollEnd) {
   ASSERT_FALSE(element().animator);
+  // Create a test animator.  The start progress of 0.0 is a dummy value, as the
+  // animator's progress properties are unused in this test.
   FullscreenScrollEndAnimator* const kAnimator =
-      [[FullscreenScrollEndAnimator alloc] init];
+      [[FullscreenScrollEndAnimator alloc] initWithStartProgress:0.0];
   observer()->FullscreenScrollEventEnded(nullptr, kAnimator);
   EXPECT_EQ(element().animator, kAnimator);
 }
diff --git a/ios/chrome/browser/ui/history/history_collection_view_controller.mm b/ios/chrome/browser/ui/history/history_collection_view_controller.mm
index 938adc5..b7332708 100644
--- a/ios/chrome/browser/ui/history/history_collection_view_controller.mm
+++ b/ios/chrome/browser/ui/history/history_collection_view_controller.mm
@@ -43,6 +43,7 @@
 #include "ios/chrome/browser/ui/history/ios_browsing_history_driver.h"
 #import "ios/chrome/browser/ui/url_loader.h"
 #import "ios/chrome/browser/ui/util/pasteboard_util.h"
+#import "ios/chrome/browser/ui/util/top_view_controller.h"
 #include "ios/chrome/grit/ios_strings.h"
 #import "ios/third_party/material_components_ios/src/components/ActivityIndicator/src/MDCActivityIndicator.h"
 #import "ios/third_party/material_components_ios/src/components/Collections/src/MaterialCollections.h"
@@ -818,9 +819,9 @@
   params.menu_title.reset([menuTitle copy]);
 
   // Present sheet/popover using controller that is added to view hierarchy.
-  UIViewController* topController = [params.view window].rootViewController;
-  while (topController.presentedViewController)
-    topController = topController.presentedViewController;
+  // TODO(crbug.com/754642): Remove TopPresentedViewController().
+  UIViewController* topController =
+      top_view_controller::TopPresentedViewController();
 
   self.contextMenuCoordinator =
       [[ContextMenuCoordinator alloc] initWithBaseViewController:topController
diff --git a/ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.h b/ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.h
index b19c6991..0f20c86 100644
--- a/ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.h
+++ b/ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.h
@@ -28,10 +28,6 @@
 
 - (instancetype)initWithCoder:(NSCoder*)aDecoder NS_UNAVAILABLE;
 
-// Delegate getter and setter.  Overridden to use OmniboxTextFieldDelegate
-// instead of UITextFieldDelegate.
-- (id<OmniboxTextFieldDelegate>)delegate;
-- (void)setDelegate:(id<OmniboxTextFieldDelegate>)delegate;
 
 // Sets the field's text to |text|.  If |userTextLength| is less than the length
 // of |text|, the excess is displayed as inline autocompleted text.  When the
@@ -64,15 +60,6 @@
 // on older version of iOS.
 - (NSString*)markedText;
 
-// Initial touch on the Omnibox triggers a "pre-edit" state. The current
-// URL is shown without any insertion point. First character typed replaces
-// the URL. A second touch turns on the insertion point. |preEditStaticLabel|
-// is normally hidden. In pre-edit state, |preEditStaticLabel| is unhidden
-// and displays the URL that will be edited on the second touch.
-- (void)enterPreEditState;
-- (void)exitPreEditState;
-- (BOOL)isPreEditing;
-
 // Returns the current selected text range as an NSRange.
 - (NSRange)selectedNSRange;
 
@@ -93,21 +80,34 @@
 // Called when animations added by |-animateFadeWithStyle:| can be removed.
 - (void)cleanUpFadeAnimations;
 
-// Redeclare the delegate property to be the more specific
-// OmniboxTextFieldDelegate.
-@property(nonatomic, weak) id<OmniboxTextFieldDelegate> delegate;
-
-@property(nonatomic, strong) NSString* preEditText;
-@property(nonatomic) BOOL clearingPreEditText;
-@property(nonatomic, strong) UIColor* selectedTextBackgroundColor;
-@property(nonatomic, strong) UIColor* placeholderTextColor;
-@property(nonatomic, assign) BOOL incognito;
-
+// New animations API. Currently are behind a flag since they require iOS 10
+// APIs to work. They replace all animations above.
 - (void)addExpandOmniboxAnimations:(UIViewPropertyAnimator*)animator
     API_AVAILABLE(ios(10.0));
 - (void)addContractOmniboxAnimations:(UIViewPropertyAnimator*)animator
     API_AVAILABLE(ios(10.0));
 
+// Initial touch on the Omnibox triggers a "pre-edit" state. The current
+// URL is shown without any insertion point. First character typed replaces
+// the URL. A second touch turns on the insertion point. |preEditStaticLabel|
+// is normally hidden. In pre-edit state, |preEditStaticLabel| is unhidden
+// and displays the URL that will be edited on the second touch.
+- (void)enterPreEditState;
+- (void)exitPreEditState;
+- (BOOL)isPreEditing;
+
+// The delegate for this textfield.  Overridden to use OmniboxTextFieldDelegate
+// instead of UITextFieldDelegate.
+@property(nonatomic, weak) id<OmniboxTextFieldDelegate> delegate;
+
+// Text displayed when in pre-edit state.
+@property(nonatomic, strong) NSString* preEditText;
+
+@property(nonatomic) BOOL clearingPreEditText;
+@property(nonatomic, strong) UIColor* selectedTextBackgroundColor;
+@property(nonatomic, strong) UIColor* placeholderTextColor;
+@property(nonatomic, assign) BOOL incognito;
+
 @end
 
 // A category for defining new methods that access private ivars.
diff --git a/ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.mm b/ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.mm
index 3d13c059..23d14134 100644
--- a/ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.mm
+++ b/ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.mm
@@ -101,6 +101,7 @@
 @synthesize placeholderTextColor = _placeholderTextColor;
 @synthesize incognito = _incognito;
 
+#pragma mark - Public methods
 // Overload to allow for code-based initialization.
 - (instancetype)initWithFrame:(CGRect)frame {
   return [self initWithFrame:frame
@@ -156,6 +157,149 @@
   return nil;
 }
 
+- (void)setText:(NSAttributedString*)text
+    userTextLength:(size_t)userTextLength {
+  DCHECK_LE(userTextLength, [text length]);
+
+  NSUInteger autocompleteLength = [text length] - userTextLength;
+  [self setTextInternal:text autocompleteLength:autocompleteLength];
+}
+
+- (void)insertTextWhileEditing:(NSString*)text {
+  // This method should only be called while editing.
+  DCHECK([self isFirstResponder]);
+
+  if ([self markedTextRange] != nil)
+    [self unmarkText];
+
+  NSRange selectedNSRange = [self selectedNSRange];
+  if (![self delegate] || [[self delegate] textField:self
+                              shouldChangeCharactersInRange:selectedNSRange
+                                          replacementString:text]) {
+    [self replaceRange:[self selectedTextRange] withText:text];
+  }
+}
+
+- (base::string16)displayedText {
+  return base::SysNSStringToUTF16([self nsDisplayedText]);
+}
+
+- (base::string16)autocompleteText {
+  DCHECK_LT([[self text] length], [[_selection text] length])
+      << "[_selection text] and [self text] are out of sync. "
+      << "Please email justincohen@ and rohitrao@ if you see this.";
+  if (_selection && [[_selection text] length] > [[self text] length]) {
+    return base::SysNSStringToUTF16(
+        [[_selection text] substringFromIndex:[[self text] length]]);
+  }
+  return base::string16();
+}
+
+- (BOOL)hasAutocompleteText {
+  return !!_selection;
+}
+
+- (void)clearAutocompleteText {
+  if (_selection) {
+    [_selection removeFromSuperview];
+    _selection = nil;
+    [self showTextAndCursor];
+  }
+}
+
+- (NSString*)markedText {
+  DCHECK([self conformsToProtocol:@protocol(UITextInput)]);
+  return [self textInRange:[self markedTextRange]];
+}
+
+- (NSRange)selectedNSRange {
+  DCHECK([self isFirstResponder]);
+  UITextPosition* beginning = [self beginningOfDocument];
+  UITextRange* selectedRange = [self selectedTextRange];
+  NSInteger start =
+      [self offsetFromPosition:beginning toPosition:[selectedRange start]];
+  NSInteger length = [self offsetFromPosition:[selectedRange start]
+                                   toPosition:[selectedRange end]];
+  return NSMakeRange(start, length);
+}
+
+- (NSTextAlignment)bestTextAlignment {
+  if ([self isFirstResponder]) {
+    return [self bestAlignmentForText:[self text]];
+  }
+  return NSTextAlignmentNatural;
+}
+
+// Normally NSTextAlignmentNatural would handle text alignment automatically,
+// but there are numerous edge case issues with it, so it's simpler to just
+// manually update the text alignment and writing direction of the UITextField.
+- (void)updateTextDirection {
+  // Setting the empty field to Natural seems to let iOS update the cursor
+  // position when the keyboard language is changed.
+  if (![self text].length) {
+    [self setTextAlignment:NSTextAlignmentNatural];
+    return;
+  }
+
+  NSTextAlignment alignment = [self bestTextAlignment];
+  [self setTextAlignment:alignment];
+  if (!base::ios::IsRunningOnIOS11OrLater()) {
+    // TODO(crbug.com/730461): Remove this entire block once it's been tested
+    // on trunk.
+    UITextWritingDirection writingDirection =
+        alignment == NSTextAlignmentLeft ? UITextWritingDirectionLeftToRight
+                                         : UITextWritingDirectionRightToLeft;
+    [self
+        setBaseWritingDirection:writingDirection
+                       forRange:
+                           [self
+                               textRangeFromPosition:[self beginningOfDocument]
+                                          toPosition:[self endOfDocument]]];
+  }
+}
+
+- (UIColor*)displayedTextColor {
+  return _displayedTextColor;
+}
+
+#pragma mark animations
+
+- (void)animateFadeWithStyle:(OmniboxTextFieldFadeStyle)style {
+  // Animation values
+  BOOL isFadingIn = (style == OMNIBOX_TEXT_FIELD_FADE_STYLE_IN);
+  CGFloat beginOpacity = isFadingIn ? 0.0 : 1.0;
+  CGFloat endOpacity = isFadingIn ? 1.0 : 0.0;
+  CAMediaTimingFunction* opacityTiming = ios::material::TimingFunction(
+      isFadingIn ? ios::material::CurveEaseOut : ios::material::CurveEaseIn);
+  CFTimeInterval delay = isFadingIn ? ios::material::kDuration8 : 0.0;
+
+  CAAnimation* labelAnimation = OpacityAnimationMake(beginOpacity, endOpacity);
+  labelAnimation.duration =
+      isFadingIn ? ios::material::kDuration6 : ios::material::kDuration8;
+  labelAnimation.timingFunction = opacityTiming;
+  labelAnimation = DelayedAnimationMake(labelAnimation, delay);
+  CAAnimation* auxillaryViewAnimation =
+      OpacityAnimationMake(beginOpacity, endOpacity);
+  auxillaryViewAnimation.duration = ios::material::kDuration8;
+  auxillaryViewAnimation.timingFunction = opacityTiming;
+  auxillaryViewAnimation = DelayedAnimationMake(auxillaryViewAnimation, delay);
+
+  for (UIView* subview in self.subviews) {
+    if ([subview isKindOfClass:[UILabel class]]) {
+      [subview.layer addAnimation:labelAnimation
+                           forKey:kOmniboxFadeAnimationKey];
+    } else {
+      [subview.layer addAnimation:auxillaryViewAnimation
+                           forKey:kOmniboxFadeAnimationKey];
+    }
+  }
+}
+
+- (void)cleanUpFadeAnimations {
+  RemoveAnimationForKeyFromLayers(kOmniboxFadeAnimationKey,
+                                  [self fadeAnimationLayers]);
+}
+
 - (void)addExpandOmniboxAnimations:(UIViewPropertyAnimator*)animator
     API_AVAILABLE(ios(10.0)) {
   __weak OmniboxTextFieldIOS* weakSelf = self;
@@ -192,68 +336,7 @@
   }];
 }
 
-// Enforces that the delegate is an OmniboxTextFieldDelegate.
-- (id<OmniboxTextFieldDelegate>)delegate {
-  id delegate = [super delegate];
-  DCHECK(delegate == nil ||
-         [[delegate class]
-             conformsToProtocol:@protocol(OmniboxTextFieldDelegate)]);
-  return delegate;
-}
-
-// Overridden to require an OmniboxTextFieldDelegate.
-- (void)setDelegate:(id<OmniboxTextFieldDelegate>)delegate {
-  [super setDelegate:delegate];
-}
-
-// Exposed for testing.
-- (UILabel*)preEditStaticLabel {
-  return _preEditStaticLabel;
-}
-
-- (void)insertTextWhileEditing:(NSString*)text {
-  // This method should only be called while editing.
-  DCHECK([self isFirstResponder]);
-
-  if ([self markedTextRange] != nil)
-    [self unmarkText];
-
-  NSRange selectedNSRange = [self selectedNSRange];
-  if (![self delegate] || [[self delegate] textField:self
-                              shouldChangeCharactersInRange:selectedNSRange
-                                          replacementString:text]) {
-    [self replaceRange:[self selectedTextRange] withText:text];
-  }
-}
-
-// Method called when the users touches the text input. This will accept the
-// autocompleted text.
-- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
-  if ([self isPreEditing]) {
-    [self exitPreEditState];
-    [super selectAll:nil];
-  }
-
-  if (!_selection) {
-    [super touchesBegan:touches withEvent:event];
-    return;
-  }
-
-  // Only consider a single touch.
-  UITouch* touch = [touches anyObject];
-  if (!touch)
-    return;
-
-  // Accept selection.
-  NSString* newText = [[self nsDisplayedText] copy];
-  [self clearAutocompleteText];
-  [self setText:newText];
-}
-
-// Gets the bounds of the rect covering the URL.
-- (CGRect)preEditLabelRectForBounds:(CGRect)bounds {
-  return [self editingRectForBounds:self.bounds];
-}
+#pragma mark pre-edit
 
 // Creates a UILabel based on the current dimension of the text field and
 // displays the URL in the UILabel so it appears properly aligned to the URL.
@@ -295,37 +378,216 @@
   [self addSubview:_preEditStaticLabel];
 }
 
-- (NSTextAlignment)bestAlignmentForText:(NSString*)text {
-  if (text.length) {
-    NSString* lang = CFBridgingRelease(CFStringTokenizerCopyBestStringLanguage(
-        (CFStringRef)text, CFRangeMake(0, text.length)));
+// Finishes pre-edit state by removing the UILabel with the URL.
+- (void)exitPreEditState {
+  [self setPreEditText:nil];
+  if (_preEditStaticLabel) {
+    [_preEditStaticLabel removeFromSuperview];
+    _preEditStaticLabel = nil;
+    [self showTextAndCursor];
+  }
+}
 
-    if ([NSLocale characterDirectionForLanguage:lang] ==
-        NSLocaleLanguageDirectionRightToLeft) {
-      return NSTextAlignmentRight;
+// Returns whether we are processing the first touch event on the text field.
+- (BOOL)isPreEditing {
+  return !![self preEditText];
+}
+
+#pragma mark - TestingUtilities category
+
+// Exposed for testing.
+- (UILabel*)preEditStaticLabel {
+  return _preEditStaticLabel;
+}
+
+#pragma mark - Properties
+
+// Enforces that the delegate is an OmniboxTextFieldDelegate.
+- (id<OmniboxTextFieldDelegate>)delegate {
+  id delegate = [super delegate];
+  DCHECK(delegate == nil ||
+         [[delegate class]
+             conformsToProtocol:@protocol(OmniboxTextFieldDelegate)]);
+  return delegate;
+}
+
+// Overridden to require an OmniboxTextFieldDelegate.
+- (void)setDelegate:(id<OmniboxTextFieldDelegate>)delegate {
+  [super setDelegate:delegate];
+}
+
+#pragma mark - Private methods
+
+#pragma mark - UITextField
+
+// Ensures that attributedText always uses the proper style attributes.
+- (void)setAttributedText:(NSAttributedString*)attributedText {
+  NSMutableAttributedString* mutableText = [attributedText mutableCopy];
+  NSRange entireString = NSMakeRange(0, [mutableText length]);
+
+  // Set the font.
+  [mutableText addAttribute:NSFontAttributeName value:_font range:entireString];
+
+  // When editing, use the default text color for all text.
+  if (self.editing) {
+    // Hide the text when the |_selection| label is displayed.
+    UIColor* textColor =
+        _selection ? [UIColor clearColor] : _displayedTextColor;
+    [mutableText addAttribute:NSForegroundColorAttributeName
+                        value:textColor
+                        range:entireString];
+  } else {
+    NSMutableParagraphStyle* style = [[NSMutableParagraphStyle alloc] init];
+    // URLs have their text direction set to to LTR (avoids RTL characters
+    // making the URL render from right to left, as per the URL rendering
+    // standard described here: https://url.spec.whatwg.org/#url-rendering
+    [style setBaseWritingDirection:NSWritingDirectionLeftToRight];
+
+    // Set linebreak mode to 'clipping' to ensure the text is never elided.
+    // This is a workaround for iOS 6, where it appears that
+    // [self.attributedText size] is not wide enough for the string (e.g. a URL
+    // else ending with '.com' will be elided to end with '.c...'). It appears
+    // to be off by one point so clipping is acceptable as it doesn't actually
+    // cut off any of the text.
+    [style setLineBreakMode:NSLineBreakByClipping];
+
+    [mutableText addAttribute:NSParagraphStyleAttributeName
+                        value:style
+                        range:entireString];
+  }
+
+  [super setAttributedText:mutableText];
+}
+
+- (void)setPlaceholder:(NSString*)placeholder {
+  if (placeholder && _placeholderTextColor) {
+    NSDictionary* attributes =
+        @{NSForegroundColorAttributeName : _placeholderTextColor};
+    self.attributedPlaceholder =
+        [[NSAttributedString alloc] initWithString:placeholder
+                                        attributes:attributes];
+  } else {
+    [super setPlaceholder:placeholder];
+  }
+}
+
+- (void)setText:(NSString*)text {
+  NSAttributedString* as = [[NSAttributedString alloc] initWithString:text];
+  if (self.text.length > 0 && as.length == 0) {
+    // Remove the fade animations before the subviews are removed.
+    [self cleanUpFadeAnimations];
+  }
+  [self setTextInternal:as autocompleteLength:0];
+}
+
+- (CGRect)textRectForBounds:(CGRect)bounds {
+  CGRect newBounds = [super textRectForBounds:bounds];
+
+  LayoutRect textRectLayout =
+      LayoutRectForRectInBoundingRect(newBounds, bounds);
+  CGFloat textInset = kTextInsetNoLeftView;
+
+  // Shift the text right and reduce the width to create empty space between the
+  // left view and the omnibox text.
+  textRectLayout.position.leading += textInset + kTextAreaLeadingOffset;
+  textRectLayout.size.width -= textInset - kTextAreaLeadingOffset;
+
+  if (IsIPadIdiom()) {
+    if (!IsCompactTablet()) {
+      // Adjust the width so that the text doesn't overlap with the bookmark and
+      // voice search buttons which are displayed inside the omnibox.
+      textRectLayout.size.width += self.rightView.bounds.size.width -
+                                   kVoiceSearchButtonWidth - kStarButtonWidth;
     }
   }
-  return NSTextAlignmentLeft;
+
+  return LayoutRectGetRect(textRectLayout);
 }
 
-- (NSTextAlignment)bestTextAlignment {
-  if ([self isFirstResponder]) {
-    return [self bestAlignmentForText:[self text]];
+- (CGRect)editingRectForBounds:(CGRect)bounds {
+  CGRect newBounds = [super editingRectForBounds:bounds];
+
+  // -editingRectForBounds doesn't account for rightViews that aren't flush
+  // with the right edge, it just looks at the rightView's width.  Account for
+  // the offset here.
+  CGFloat rightViewMaxX = CGRectGetMaxX([self rightViewRectForBounds:bounds]);
+  if (rightViewMaxX)
+    newBounds.size.width -= bounds.size.width - rightViewMaxX;
+
+  LayoutRect editingRectLayout =
+      LayoutRectForRectInBoundingRect(newBounds, bounds);
+  editingRectLayout.position.leading += kTextAreaLeadingOffset;
+  editingRectLayout.position.leading += kTextInset;
+  editingRectLayout.size.width -= kTextInset + kEditingRectWidthInset;
+  if (IsIPadIdiom()) {
+    if (!IsCompactTablet() && !self.rightView) {
+      // Normally the clear button shrinks the edit box, but if the rightView
+      // isn't set, shrink behind the mic icons.
+      editingRectLayout.size.width -= kVoiceSearchButtonWidth;
+    }
+  } else {
+    CGFloat xDiff = editingRectLayout.position.leading - kEditingRectX;
+    editingRectLayout.position.leading = kEditingRectX;
+    editingRectLayout.size.width += xDiff;
   }
-  return NSTextAlignmentNatural;
+  // Don't let the edit rect extend over the clear button.  The right view
+  // is hidden during animations, so fake its width here.
+  if (self.rightViewMode == UITextFieldViewModeNever)
+    editingRectLayout.size.width -= self.rightView.bounds.size.width;
+
+  newBounds = LayoutRectGetRect(editingRectLayout);
+
+  // Position the selection view appropriately.
+  [_selection setFrame:newBounds];
+
+  return newBounds;
 }
 
-- (NSTextAlignment)preEditTextAlignment {
-  // If the pre-edit text is wider than the omnibox, right-align the text so it
-  // ends at the same x coord as the blue selection box.
-  CGSize textSize =
-      [_preEditStaticLabel.text cr_pixelAlignedSizeWithFont:_font];
-  // Note, this does not need to support RTL, as URLs are always LTR.
-  return textSize.width < _preEditStaticLabel.frame.size.width
-             ? NSTextAlignmentLeft
-             : NSTextAlignmentRight;
+// Enumerate url components (host, path) and draw each one in different rect.
+- (void)drawTextInRect:(CGRect)rect {
+  if (base::ios::IsRunningOnOrLater(11, 1, 0)) {
+    // -[UITextField drawTextInRect:] ignores the argument, so we can't do
+    // anything on 11.1 and up.
+    [super drawTextInRect:rect];
+    return;
+  }
+
+  // Save and restore the graphics state because rectForDrawTextInRect may
+  // apply an image mask to fade out beginning and/or end of the URL.
+  gfx::ScopedCGContextSaveGState saver(UIGraphicsGetCurrentContext());
+  [super drawTextInRect:[self rectForDrawTextInRect:rect]];
 }
 
+// Overriding this method to offset the rightView property
+// (containing a clear text button).
+- (CGRect)rightViewRectForBounds:(CGRect)bounds {
+  // iOS9 added updated RTL support, but only half implemented it for
+  // UITextField. leftView and rightView were not renamed, but are are correctly
+  // swapped and treated as leadingView / trailingView.  However,
+  // -leftViewRectForBounds and -rightViewRectForBounds are *not* treated as
+  // leading and trailing.  Hence the swapping below.
+  if ([self isTextFieldLTR]) {
+    return [self layoutRightViewForBounds:bounds];
+  }
+  return [self layoutLeftViewForBounds:bounds];
+}
+
+// Overriding this method to offset the leftView property
+// (containing a placeholder image) consistently with omnibox text padding.
+- (CGRect)leftViewRectForBounds:(CGRect)bounds {
+  // iOS9 added updated RTL support, but only half implemented it for
+  // UITextField. leftView and rightView were not renamed, but are are correctly
+  // swapped and treated as leadingView / trailingView.  However,
+  // -leftViewRectForBounds and -rightViewRectForBounds are *not* treated as
+  // leading and trailing.  Hence the swapping below.
+  if ([self isTextFieldLTR]) {
+    return [self layoutLeftViewForBounds:bounds];
+  }
+  return [self layoutRightViewForBounds:bounds];
+}
+
+#pragma mark - UIView
+
 - (void)layoutSubviews {
   [super layoutSubviews];
   if ([self isPreEditing]) {
@@ -340,44 +602,38 @@
   }
 }
 
-// Finishes pre-edit state by removing the UILabel with the URL.
-- (void)exitPreEditState {
-  [self setPreEditText:nil];
-  if (_preEditStaticLabel) {
-    [_preEditStaticLabel removeFromSuperview];
-    _preEditStaticLabel = nil;
-    [self showTextAndCursor];
+- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
+  // Anything in the narrow bar above OmniboxTextFieldIOS view
+  // will also activate the text field.
+  if (point.y < 0)
+    point.y = 0;
+  return [super hitTest:point withEvent:event];
+}
+
+#pragma mark - UIResponder
+
+// Method called when the users touches the text input. This will accept the
+// autocompleted text.
+- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
+  if ([self isPreEditing]) {
+    [self exitPreEditState];
+    [super selectAll:nil];
   }
-}
 
-- (UIColor*)displayedTextColor {
-  return _displayedTextColor;
-}
-
-// Returns whether we are processing the first touch event on the text field.
-- (BOOL)isPreEditing {
-  return !![self preEditText];
-}
-
-- (NSString*)nsDisplayedText {
-  if (_selection)
-    return [_selection text];
-  return [self text];
-}
-
-- (base::string16)displayedText {
-  return base::SysNSStringToUTF16([self nsDisplayedText]);
-}
-
-- (base::string16)autocompleteText {
-  DCHECK_LT([[self text] length], [[_selection text] length])
-      << "[_selection text] and [self text] are out of sync. "
-      << "Please email justincohen@ and rohitrao@ if you see this.";
-  if (_selection && [[_selection text] length] > [[self text] length]) {
-    return base::SysNSStringToUTF16(
-        [[_selection text] substringFromIndex:[[self text] length]]);
+  if (!_selection) {
+    [super touchesBegan:touches withEvent:event];
+    return;
   }
-  return base::string16();
+
+  // Only consider a single touch.
+  UITouch* touch = [touches anyObject];
+  if (!touch)
+    return;
+
+  // Accept selection.
+  NSString* newText = [[self nsDisplayedText] copy];
+  [self clearAutocompleteText];
+  [self setText:newText];
 }
 
 - (void)select:(id)sender {
@@ -399,6 +655,94 @@
   [super selectAll:sender];
 }
 
+- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
+  // Disable the "Define" menu item.  iOS7 implements this with a private
+  // selector.  Avoid using private APIs by instead doing a string comparison.
+  if ([NSStringFromSelector(action) hasSuffix:@"define:"]) {
+    return NO;
+  }
+
+  // Disable the RTL arrow menu item. The omnibox sets alignment based on the
+  // text in the field, and should not be overridden.
+  if ([NSStringFromSelector(action) hasPrefix:@"makeTextWritingDirection"]) {
+    return NO;
+  }
+
+  return [super canPerformAction:action withSender:sender];
+}
+
+#pragma mark Copy/Paste
+
+// Overridden to allow for custom omnibox copy behavior.  This includes
+// preprending http:// to the copied URL if needed.
+- (void)copy:(id)sender {
+  id<OmniboxTextFieldDelegate> delegate = [self delegate];
+  BOOL handled = NO;
+
+  // Must test for the onCopy method, since it's optional.
+  if ([delegate respondsToSelector:@selector(onCopy)])
+    handled = [delegate onCopy];
+
+  // iOS 4 doesn't expose an API that allows the delegate to handle the copy
+  // operation, so let the superclass perform the copy if the delegate couldn't.
+  if (!handled)
+    [super copy:sender];
+}
+
+// Overridden to notify the delegate that a paste is in progress.
+- (void)paste:(id)sender {
+  id delegate = [self delegate];
+  if ([delegate respondsToSelector:@selector(willPaste)])
+    [delegate willPaste];
+  [super paste:sender];
+}
+
+#pragma mark UIKeyInput
+
+- (void)deleteBackward {
+  // Must test for the onDeleteBackward method, since it's optional.
+  if ([[self delegate] respondsToSelector:@selector(onDeleteBackward)])
+    [[self delegate] onDeleteBackward];
+  [super deleteBackward];
+}
+
+#pragma mark - helpers
+
+// Gets the bounds of the rect covering the URL.
+- (CGRect)preEditLabelRectForBounds:(CGRect)bounds {
+  return [self editingRectForBounds:self.bounds];
+}
+
+- (NSTextAlignment)bestAlignmentForText:(NSString*)text {
+  if (text.length) {
+    NSString* lang = CFBridgingRelease(CFStringTokenizerCopyBestStringLanguage(
+        (CFStringRef)text, CFRangeMake(0, text.length)));
+
+    if ([NSLocale characterDirectionForLanguage:lang] ==
+        NSLocaleLanguageDirectionRightToLeft) {
+      return NSTextAlignmentRight;
+    }
+  }
+  return NSTextAlignmentLeft;
+}
+
+- (NSTextAlignment)preEditTextAlignment {
+  // If the pre-edit text is wider than the omnibox, right-align the text so it
+  // ends at the same x coord as the blue selection box.
+  CGSize textSize =
+      [_preEditStaticLabel.text cr_pixelAlignedSizeWithFont:_font];
+  // Note, this does not need to support RTL, as URLs are always LTR.
+  return textSize.width < _preEditStaticLabel.frame.size.width
+             ? NSTextAlignmentLeft
+             : NSTextAlignmentRight;
+}
+
+- (NSString*)nsDisplayedText {
+  if (_selection)
+    return [_selection text];
+  return [self text];
+}
+
 // Creates the SelectedTextLabel if it doesn't already exist and adds it as a
 // subview.
 - (void)createSelectionViewIfNecessary {
@@ -414,13 +758,6 @@
   [self hideTextAndCursor];
 }
 
-- (void)deleteBackward {
-  // Must test for the onDeleteBackward method, since it's optional.
-  if ([[self delegate] respondsToSelector:@selector(onDeleteBackward)])
-    [[self delegate] onDeleteBackward];
-  [super deleteBackward];
-}
-
 // Helper method used to set the text of this field.  Updates the selection view
 // to contain the correct inline autocomplete text.
 - (void)setTextInternal:(NSAttributedString*)text
@@ -490,114 +827,6 @@
                                                         alpha:1.0];
 }
 
-// Ensures that attributedText always uses the proper style attributes.
-- (void)setAttributedText:(NSAttributedString*)attributedText {
-  NSMutableAttributedString* mutableText = [attributedText mutableCopy];
-  NSRange entireString = NSMakeRange(0, [mutableText length]);
-
-  // Set the font.
-  [mutableText addAttribute:NSFontAttributeName value:_font range:entireString];
-
-  // When editing, use the default text color for all text.
-  if (self.editing) {
-    // Hide the text when the |_selection| label is displayed.
-    UIColor* textColor =
-        _selection ? [UIColor clearColor] : _displayedTextColor;
-    [mutableText addAttribute:NSForegroundColorAttributeName
-                        value:textColor
-                        range:entireString];
-  } else {
-    NSMutableParagraphStyle* style = [[NSMutableParagraphStyle alloc] init];
-    // URLs have their text direction set to to LTR (avoids RTL characters
-    // making the URL render from right to left, as per the URL rendering
-    // standard described here: https://url.spec.whatwg.org/#url-rendering
-    [style setBaseWritingDirection:NSWritingDirectionLeftToRight];
-
-    // Set linebreak mode to 'clipping' to ensure the text is never elided.
-    // This is a workaround for iOS 6, where it appears that
-    // [self.attributedText size] is not wide enough for the string (e.g. a URL
-    // else ending with '.com' will be elided to end with '.c...'). It appears
-    // to be off by one point so clipping is acceptable as it doesn't actually
-    // cut off any of the text.
-    [style setLineBreakMode:NSLineBreakByClipping];
-
-    [mutableText addAttribute:NSParagraphStyleAttributeName
-                        value:style
-                        range:entireString];
-  }
-
-  [super setAttributedText:mutableText];
-}
-
-// Normally NSTextAlignmentNatural would handle text alignment automatically,
-// but there are numerous edge case issues with it, so it's simpler to just
-// manually update the text alignment and writing direction of the UITextField.
-- (void)updateTextDirection {
-  // Setting the empty field to Natural seems to let iOS update the cursor
-  // position when the keyboard language is changed.
-  if (![self text].length) {
-    [self setTextAlignment:NSTextAlignmentNatural];
-    return;
-  }
-
-  NSTextAlignment alignment = [self bestTextAlignment];
-  [self setTextAlignment:alignment];
-  if (!base::ios::IsRunningOnIOS11OrLater()) {
-    // TODO(crbug.com/730461): Remove this entire block once it's been tested
-    // on trunk.
-    UITextWritingDirection writingDirection =
-        alignment == NSTextAlignmentLeft ? UITextWritingDirectionLeftToRight
-                                         : UITextWritingDirectionRightToLeft;
-    [self
-        setBaseWritingDirection:writingDirection
-                       forRange:
-                           [self
-                               textRangeFromPosition:[self beginningOfDocument]
-                                          toPosition:[self endOfDocument]]];
-  }
-}
-
-- (void)setPlaceholder:(NSString*)placeholder {
-  if (placeholder && _placeholderTextColor) {
-    NSDictionary* attributes =
-        @{NSForegroundColorAttributeName : _placeholderTextColor};
-    self.attributedPlaceholder =
-        [[NSAttributedString alloc] initWithString:placeholder
-                                        attributes:attributes];
-  } else {
-    [super setPlaceholder:placeholder];
-  }
-}
-
-- (void)setText:(NSString*)text {
-  NSAttributedString* as = [[NSAttributedString alloc] initWithString:text];
-  if (self.text.length > 0 && as.length == 0) {
-    // Remove the fade animations before the subviews are removed.
-    [self cleanUpFadeAnimations];
-  }
-  [self setTextInternal:as autocompleteLength:0];
-}
-
-- (void)setText:(NSAttributedString*)text
-    userTextLength:(size_t)userTextLength {
-  DCHECK_LE(userTextLength, [text length]);
-
-  NSUInteger autocompleteLength = [text length] - userTextLength;
-  [self setTextInternal:text autocompleteLength:autocompleteLength];
-}
-
-- (BOOL)hasAutocompleteText {
-  return !!_selection;
-}
-
-- (void)clearAutocompleteText {
-  if (_selection) {
-    [_selection removeFromSuperview];
-    _selection = nil;
-    [self showTextAndCursor];
-  }
-}
-
 - (BOOL)isColorHidden:(UIColor*)color {
   return ([color isEqual:[UIColor clearColor]] ||
           CGColorGetAlpha(color.CGColor) < 0.05);
@@ -621,74 +850,6 @@
   [self setTextColor:[UIColor clearColor]];
 }
 
-- (NSString*)markedText {
-  DCHECK([self conformsToProtocol:@protocol(UITextInput)]);
-  return [self textInRange:[self markedTextRange]];
-}
-
-- (CGRect)textRectForBounds:(CGRect)bounds {
-  CGRect newBounds = [super textRectForBounds:bounds];
-
-  LayoutRect textRectLayout =
-      LayoutRectForRectInBoundingRect(newBounds, bounds);
-  CGFloat textInset = kTextInsetNoLeftView;
-
-  // Shift the text right and reduce the width to create empty space between the
-  // left view and the omnibox text.
-  textRectLayout.position.leading += textInset + kTextAreaLeadingOffset;
-  textRectLayout.size.width -= textInset - kTextAreaLeadingOffset;
-
-  if (IsIPadIdiom()) {
-    if (!IsCompactTablet()) {
-      // Adjust the width so that the text doesn't overlap with the bookmark and
-      // voice search buttons which are displayed inside the omnibox.
-      textRectLayout.size.width += self.rightView.bounds.size.width -
-                                   kVoiceSearchButtonWidth - kStarButtonWidth;
-    }
-  }
-
-  return LayoutRectGetRect(textRectLayout);
-}
-
-- (CGRect)editingRectForBounds:(CGRect)bounds {
-  CGRect newBounds = [super editingRectForBounds:bounds];
-
-  // -editingRectForBounds doesn't account for rightViews that aren't flush
-  // with the right edge, it just looks at the rightView's width.  Account for
-  // the offset here.
-  CGFloat rightViewMaxX = CGRectGetMaxX([self rightViewRectForBounds:bounds]);
-  if (rightViewMaxX)
-    newBounds.size.width -= bounds.size.width - rightViewMaxX;
-
-  LayoutRect editingRectLayout =
-      LayoutRectForRectInBoundingRect(newBounds, bounds);
-  editingRectLayout.position.leading += kTextAreaLeadingOffset;
-  editingRectLayout.position.leading += kTextInset;
-  editingRectLayout.size.width -= kTextInset + kEditingRectWidthInset;
-  if (IsIPadIdiom()) {
-    if (!IsCompactTablet() && !self.rightView) {
-      // Normally the clear button shrinks the edit box, but if the rightView
-      // isn't set, shrink behind the mic icons.
-      editingRectLayout.size.width -= kVoiceSearchButtonWidth;
-    }
-  } else {
-    CGFloat xDiff = editingRectLayout.position.leading - kEditingRectX;
-    editingRectLayout.position.leading = kEditingRectX;
-    editingRectLayout.size.width += xDiff;
-  }
-  // Don't let the edit rect extend over the clear button.  The right view
-  // is hidden during animations, so fake its width here.
-  if (self.rightViewMode == UITextFieldViewModeNever)
-    editingRectLayout.size.width -= self.rightView.bounds.size.width;
-
-  newBounds = LayoutRectGetRect(editingRectLayout);
-
-  // Position the selection view appropriately.
-  [_selection setFrame:newBounds];
-
-  return newBounds;
-}
-
 - (CGRect)rectForDrawTextInRect:(CGRect)rect {
   // The goal is to always show the most significant part of the hostname
   // (i.e. the end of the TLD).
@@ -752,27 +913,11 @@
   return rect;
 }
 
-// Enumerate url components (host, path) and draw each one in different rect.
-- (void)drawTextInRect:(CGRect)rect {
-  if (base::ios::IsRunningOnOrLater(11, 1, 0)) {
-    // -[UITextField drawTextInRect:] ignores the argument, so we can't do
-    // anything on 11.1 and up.
-    [super drawTextInRect:rect];
-    return;
-  }
-
-  // Save and restore the graphics state because rectForDrawTextInRect may
-  // apply an image mask to fade out beginning and/or end of the URL.
-  gfx::ScopedCGContextSaveGState saver(UIGraphicsGetCurrentContext());
-  [super drawTextInRect:[self rectForDrawTextInRect:rect]];
-}
-
-- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
-  // Anything in the narrow bar above OmniboxTextFieldIOS view
-  // will also activate the text field.
-  if (point.y < 0)
-    point.y = 0;
-  return [super hitTest:point withEvent:event];
+- (NSArray*)fadeAnimationLayers {
+  NSMutableArray* layers = [NSMutableArray array];
+  for (UIView* subview in self.subviews)
+    [layers addObject:subview.layer];
+  return layers;
 }
 
 - (BOOL)isTextFieldLTR {
@@ -781,20 +926,6 @@
          UIUserInterfaceLayoutDirectionLeftToRight;
 }
 
-// Overriding this method to offset the rightView property
-// (containing a clear text button).
-- (CGRect)rightViewRectForBounds:(CGRect)bounds {
-  // iOS9 added updated RTL support, but only half implemented it for
-  // UITextField. leftView and rightView were not renamed, but are are correctly
-  // swapped and treated as leadingView / trailingView.  However,
-  // -leftViewRectForBounds and -rightViewRectForBounds are *not* treated as
-  // leading and trailing.  Hence the swapping below.
-  if ([self isTextFieldLTR]) {
-    return [self layoutRightViewForBounds:bounds];
-  }
-  return [self layoutLeftViewForBounds:bounds];
-}
-
 - (CGRect)layoutRightViewForBounds:(CGRect)bounds {
   if ([self rightView]) {
     CGSize rightViewSize = self.rightView.bounds.size;
@@ -817,118 +948,8 @@
   return CGRectZero;
 }
 
-// Overriding this method to offset the leftView property
-// (containing a placeholder image) consistently with omnibox text padding.
-- (CGRect)leftViewRectForBounds:(CGRect)bounds {
-  // iOS9 added updated RTL support, but only half implemented it for
-  // UITextField. leftView and rightView were not renamed, but are are correctly
-  // swapped and treated as leadingView / trailingView.  However,
-  // -leftViewRectForBounds and -rightViewRectForBounds are *not* treated as
-  // leading and trailing.  Hence the swapping below.
-  if ([self isTextFieldLTR]) {
-    return [self layoutLeftViewForBounds:bounds];
-  }
-  return [self layoutRightViewForBounds:bounds];
-}
-
 - (CGRect)layoutLeftViewForBounds:(CGRect)bounds {
   return CGRectZero;
 }
 
-- (void)animateFadeWithStyle:(OmniboxTextFieldFadeStyle)style {
-  // Animation values
-  BOOL isFadingIn = (style == OMNIBOX_TEXT_FIELD_FADE_STYLE_IN);
-  CGFloat beginOpacity = isFadingIn ? 0.0 : 1.0;
-  CGFloat endOpacity = isFadingIn ? 1.0 : 0.0;
-  CAMediaTimingFunction* opacityTiming = ios::material::TimingFunction(
-      isFadingIn ? ios::material::CurveEaseOut : ios::material::CurveEaseIn);
-  CFTimeInterval delay = isFadingIn ? ios::material::kDuration8 : 0.0;
-
-  CAAnimation* labelAnimation = OpacityAnimationMake(beginOpacity, endOpacity);
-  labelAnimation.duration =
-      isFadingIn ? ios::material::kDuration6 : ios::material::kDuration8;
-  labelAnimation.timingFunction = opacityTiming;
-  labelAnimation = DelayedAnimationMake(labelAnimation, delay);
-  CAAnimation* auxillaryViewAnimation =
-      OpacityAnimationMake(beginOpacity, endOpacity);
-  auxillaryViewAnimation.duration = ios::material::kDuration8;
-  auxillaryViewAnimation.timingFunction = opacityTiming;
-  auxillaryViewAnimation = DelayedAnimationMake(auxillaryViewAnimation, delay);
-
-  for (UIView* subview in self.subviews) {
-    if ([subview isKindOfClass:[UILabel class]]) {
-      [subview.layer addAnimation:labelAnimation
-                           forKey:kOmniboxFadeAnimationKey];
-    } else {
-      [subview.layer addAnimation:auxillaryViewAnimation
-                           forKey:kOmniboxFadeAnimationKey];
-    }
-  }
-}
-
-- (NSArray*)fadeAnimationLayers {
-  NSMutableArray* layers = [NSMutableArray array];
-  for (UIView* subview in self.subviews)
-    [layers addObject:subview.layer];
-  return layers;
-}
-
-- (void)cleanUpFadeAnimations {
-  RemoveAnimationForKeyFromLayers(kOmniboxFadeAnimationKey,
-                                  [self fadeAnimationLayers]);
-}
-
-#pragma mark - Copy/Paste
-
-// Overridden to allow for custom omnibox copy behavior.  This includes
-// preprending http:// to the copied URL if needed.
-- (void)copy:(id)sender {
-  id<OmniboxTextFieldDelegate> delegate = [self delegate];
-  BOOL handled = NO;
-
-  // Must test for the onCopy method, since it's optional.
-  if ([delegate respondsToSelector:@selector(onCopy)])
-    handled = [delegate onCopy];
-
-  // iOS 4 doesn't expose an API that allows the delegate to handle the copy
-  // operation, so let the superclass perform the copy if the delegate couldn't.
-  if (!handled)
-    [super copy:sender];
-}
-
-// Overridden to notify the delegate that a paste is in progress.
-- (void)paste:(id)sender {
-  id delegate = [self delegate];
-  if ([delegate respondsToSelector:@selector(willPaste)])
-    [delegate willPaste];
-  [super paste:sender];
-}
-
-- (NSRange)selectedNSRange {
-  DCHECK([self isFirstResponder]);
-  UITextPosition* beginning = [self beginningOfDocument];
-  UITextRange* selectedRange = [self selectedTextRange];
-  NSInteger start =
-      [self offsetFromPosition:beginning toPosition:[selectedRange start]];
-  NSInteger length = [self offsetFromPosition:[selectedRange start]
-                                   toPosition:[selectedRange end]];
-  return NSMakeRange(start, length);
-}
-
-- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
-  // Disable the "Define" menu item.  iOS7 implements this with a private
-  // selector.  Avoid using private APIs by instead doing a string comparison.
-  if ([NSStringFromSelector(action) hasSuffix:@"define:"]) {
-    return NO;
-  }
-
-  // Disable the RTL arrow menu item. The omnibox sets alignment based on the
-  // text in the field, and should not be overridden.
-  if ([NSStringFromSelector(action) hasPrefix:@"makeTextWritingDirection"]) {
-    return NO;
-  }
-
-  return [super canPerformAction:action withSender:sender];
-}
-
 @end
diff --git a/ios/chrome/browser/ui/tab_switcher/tab_switcher_transition_egtest.mm b/ios/chrome/browser/ui/tab_switcher/tab_switcher_transition_egtest.mm
index 13b54618..ded8ecb 100644
--- a/ios/chrome/browser/ui/tab_switcher/tab_switcher_transition_egtest.mm
+++ b/ios/chrome/browser/ui/tab_switcher/tab_switcher_transition_egtest.mm
@@ -132,6 +132,14 @@
 // to fail.
 @implementation TabSwitcherTransitionTestCase
 
+// Rotate the device back to portrait if needed, since some tests attempt to run
+// in landscape.
+- (void)tearDown {
+  [EarlGrey rotateDeviceToOrientation:UIDeviceOrientationPortrait
+                           errorOrNil:nil];
+  [super tearDown];
+}
+
 // Sets up the EmbeddedTestServer as needed for tests.
 - (void)setUpTestServer {
   self.testServer->RegisterDefaultHandler(
@@ -446,4 +454,36 @@
   [ChromeEarlGreyUI openNewTab];
 }
 
+// Tests rotating the device while the switcher is not active.  This is a
+// regression test case for https://crbug.com/789975.
+- (void)testRotationsWhileSwitcherIsNotActive {
+  NSString* tab_title = @"NormalTabLongerStringForTest1";
+  [self setUpTestServer];
+  [ChromeEarlGrey loadURL:[self makeURLForTitle:tab_title]];
+
+  // Show the tab switcher and return to the BVC, in portrait.
+  [EarlGrey rotateDeviceToOrientation:UIDeviceOrientationPortrait
+                           errorOrNil:nil];
+  ShowTabSwitcher();
+  SelectTab(tab_title);
+  [ChromeEarlGrey
+      waitForWebViewContainingText:base::SysNSStringToUTF8(tab_title)];
+
+  // Show the tab switcher and return to the BVC, in landscape.
+  [EarlGrey rotateDeviceToOrientation:UIDeviceOrientationLandscapeLeft
+                           errorOrNil:nil];
+  ShowTabSwitcher();
+  SelectTab(tab_title);
+  [ChromeEarlGrey
+      waitForWebViewContainingText:base::SysNSStringToUTF8(tab_title)];
+
+  // Show the tab switcher and return to the BVC, in portrait.
+  [EarlGrey rotateDeviceToOrientation:UIDeviceOrientationPortrait
+                           errorOrNil:nil];
+  ShowTabSwitcher();
+  SelectTab(tab_title);
+  [ChromeEarlGrey
+      waitForWebViewContainingText:base::SysNSStringToUTF8(tab_title)];
+}
+
 @end
diff --git a/ios/web/web_state/js/resources/common.js b/ios/web/web_state/js/resources/common.js
index 0784d37b..43eaf3a2 100644
--- a/ios/web/web_state/js/resources/common.js
+++ b/ios/web/web_state/js/resources/common.js
@@ -230,6 +230,38 @@
            element.type === 'number';
   };
 
+ /**
+  * Sets the value of a data-bound input using AngularJS.
+  *
+  * The method first set the value using the val() method. Then, if input is
+  * bound to a model value, it sets the model value.
+  * Documentation of relevant modules of AngularJS can be found at
+  * https://docs.angularjs.org/guide/databinding
+  * https://docs.angularjs.org/api/auto/service/$injector
+  * https://docs.angularjs.org/api/ng/service/$parse
+  *
+  * @param {string} value The value the input element will be set.
+  * @param {Element} input The input element of which the value is set.
+  **/
+  function setInputElementAngularValue_(value, input) {
+    if (!input || !window['angular']) {
+      return;
+    }
+    var angular_element = window['angular'].element(input);
+    if (!angular_element) {
+      return;
+    }
+    angular_element.val(value);
+    var angular_model = angular_element.data('ngModel');
+    if (!angular_model) {
+      return;
+    }
+    angular_element.injector().invoke(['$parse', function(parse) {
+      var setter = parse(angular_model);
+      setter.assign(angular_element.scope(), value);
+    }])
+  }
+
   /**
    * Sets the value of an input and dispatches a change event if
    * |shouldSendChangeEvent|.
@@ -245,19 +277,16 @@
    *    void setChecked(bool nowChecked, TextFieldEventBehavior eventBehavior)
    * in chromium/src/third_party/WebKit/Source/core/html/HTMLInputElement.cpp.
    *
-   * @param {(string|boolean)} value The value the input element will be set.
-   *     For text input, it is the value to set in the field.
-   *     For select, it is the value of the option to select.
-   *     For checkable element, it is the checked value (true/false).
+   * @param {string} value The value the input element will be set.
    * @param {Element} input The input element of which the value is set.
    * @param {boolean} shouldSendChangeEvent Whether a change event should be
    *     dispatched.
    */
   __gCrWeb.common.setInputElementValue = function(
       value, input, shouldSendChangeEvent) {
-     if (!input) {
-       return;
-     }
+    if (!input) {
+      return;
+    }
     var changed = false;
     if (input.type === 'checkbox' || input.type === 'radio') {
       changed = input.checked !== value;
@@ -271,10 +300,15 @@
       // autofill and this method is only used for autofill for now, there is no
       // such check in this implementation.
       var sanitizedValue = __gCrWeb.common.sanitizeValueForInputElement(
-          /** @type {string} */ (value), input);
+          value, input);
       changed = sanitizedValue !== input.value;
       input.value = sanitizedValue;
     }
+    if (window['angular']) {
+      // The page uses the AngularJS framework. Update the angular value before
+      // sending events.
+      setInputElementAngularValue_(value, input);
+    }
     if (changed && shouldSendChangeEvent) {
       __gCrWeb.common.notifyElementValueChanged(input);
     }
diff --git a/media/audio/mac/audio_manager_mac.cc b/media/audio/mac/audio_manager_mac.cc
index 1b7b8d24..a0abdad 100644
--- a/media/audio/mac/audio_manager_mac.cc
+++ b/media/audio/mac/audio_manager_mac.cc
@@ -1108,13 +1108,39 @@
   OSSTATUS_DLOG_IF(WARNING, result != noErr, result)
       << "Could not get audio device latency.";
 
-  property_address.mSelector = kAudioStreamPropertyLatency;
+  // Retrieve stream ids and take the stream latency from the first stream.
+  // There may be multiple streams with different latencies, but since we're
+  // likely using this delay information for a/v sync we must choose one of
+  // them; Apple recommends just taking the first entry.
+  //
+  // TODO(dalecurtis): Refactor all these "get data size" + "get data" calls
+  // into a common utility function that just returns a std::unique_ptr.
   UInt32 stream_latency_frames = 0;
-  size = sizeof(stream_latency_frames);
-  result = AudioObjectGetPropertyData(device_id, &property_address, 0, nullptr,
-                                      &size, &stream_latency_frames);
-  OSSTATUS_DLOG_IF(WARNING, result != noErr, result)
-      << "Could not get stream latency.";
+  property_address.mSelector = kAudioDevicePropertyStreams;
+  result = AudioObjectGetPropertyDataSize(device_id, &property_address, 0,
+                                          nullptr, &size);
+  if (result == noErr && size >= sizeof(AudioStreamID)) {
+    std::unique_ptr<uint8_t[]> stream_id_storage(new uint8_t[size]);
+    AudioStreamID* stream_ids =
+        reinterpret_cast<AudioStreamID*>(stream_id_storage.get());
+    result = AudioObjectGetPropertyData(device_id, &property_address, 0,
+                                        nullptr, &size, stream_ids);
+    if (result == noErr) {
+      property_address.mSelector = kAudioStreamPropertyLatency;
+      size = sizeof(stream_latency_frames);
+      result =
+          AudioObjectGetPropertyData(stream_ids[0], &property_address, 0,
+                                     nullptr, &size, &stream_latency_frames);
+      OSSTATUS_DLOG_IF(WARNING, result != noErr, result)
+          << "Could not get stream latency for stream #0.";
+    } else {
+      OSSTATUS_DLOG(WARNING, result)
+          << "Could not get audio device stream ids.";
+    }
+  } else {
+    OSSTATUS_DLOG_IF(WARNING, result != noErr, result)
+        << "Could not get audio device stream ids size.";
+  }
 
   return base::TimeDelta::FromSecondsD(audio_unit_latency_sec) +
          AudioTimestampHelper::FramesToTime(
diff --git a/remoting/protocol/webrtc_dummy_video_encoder.cc b/remoting/protocol/webrtc_dummy_video_encoder.cc
index b10b396..d0e2b50 100644
--- a/remoting/protocol/webrtc_dummy_video_encoder.cc
+++ b/remoting/protocol/webrtc_dummy_video_encoder.cc
@@ -11,6 +11,7 @@
 #include "base/callback.h"
 #include "base/logging.h"
 #include "base/memory/ptr_util.h"
+#include "base/rand_util.h"
 #include "base/stl_util.h"
 #include "base/synchronization/lock.h"
 #include "base/threading/thread_task_runner_handle.h"
@@ -58,7 +59,11 @@
     base::WeakPtr<VideoChannelStateObserver> video_channel_state_observer)
     : main_task_runner_(main_task_runner),
       state_(kUninitialized),
-      video_channel_state_observer_(video_channel_state_observer) {}
+      video_channel_state_observer_(video_channel_state_observer) {
+  // Initialize randomly to avoid replay attacks.
+  base::RandBytes(&picture_id_, sizeof(picture_id_));
+  picture_id_ &= 0x7fff;
+}
 
 WebrtcDummyVideoEncoder::~WebrtcDummyVideoEncoder() = default;
 
@@ -163,7 +168,8 @@
     vp8_info->simulcastIdx = 0;
     vp8_info->temporalIdx = webrtc::kNoTemporalIdx;
     vp8_info->tl0PicIdx = webrtc::kNoTl0PicIdx;
-    vp8_info->pictureId = webrtc::kNoPictureId;
+    vp8_info->pictureId = picture_id_;
+    picture_id_ = (picture_id_ + 1) & 0x7fff;
   } else if (frame.codec == webrtc::kVideoCodecVP9) {
     webrtc::CodecSpecificInfoVP9* vp9_info =
         &codec_specific_info.codecSpecific.VP9;
@@ -179,7 +185,8 @@
     vp9_info->temporal_idx = webrtc::kNoTemporalIdx;
     vp9_info->spatial_idx = webrtc::kNoSpatialIdx;
     vp9_info->tl0_pic_idx = webrtc::kNoTl0PicIdx;
-    vp9_info->picture_id = webrtc::kNoPictureId;
+    vp9_info->picture_id = picture_id_;
+    picture_id_ = (picture_id_ + 1) & 0x7fff;
   } else if (frame.codec == webrtc::kVideoCodecH264) {
 #if defined(USE_H264_ENCODER)
     webrtc::CodecSpecificInfoH264* h264_info =
diff --git a/remoting/protocol/webrtc_dummy_video_encoder.h b/remoting/protocol/webrtc_dummy_video_encoder.h
index eae4bce..c8d2f468 100644
--- a/remoting/protocol/webrtc_dummy_video_encoder.h
+++ b/remoting/protocol/webrtc_dummy_video_encoder.h
@@ -64,6 +64,10 @@
   webrtc::EncodedImageCallback* encoded_callback_ = nullptr;
 
   base::WeakPtr<VideoChannelStateObserver> video_channel_state_observer_;
+
+  // 15-bit incrementing ID applied to RTP payload for each video frame when
+  // VPX is used.
+  uint16_t picture_id_ = 0;
 };
 
 // This is the external encoder factory implementation that is passed to
diff --git a/sandbox/linux/syscall_broker/broker_client.cc b/sandbox/linux/syscall_broker/broker_client.cc
index 277e61a..152dceb 100644
--- a/sandbox/linux/syscall_broker/broker_client.cc
+++ b/sandbox/linux/syscall_broker/broker_client.cc
@@ -229,5 +229,50 @@
   return return_value;
 }
 
+int BrokerClient::Readlink(const char* path, char* buf, size_t bufsize) {
+  if (fast_check_in_client_) {
+    bool ignore;
+    if (!broker_policy_.GetFileNameIfAllowedToOpen(path, O_RDONLY, nullptr,
+                                                   &ignore)) {
+      return -broker_policy_.denied_errno();
+    }
+  }
+
+  base::Pickle write_pickle;
+  write_pickle.WriteInt(COMMAND_READLINK);
+  write_pickle.WriteString(path);
+  RAW_CHECK(write_pickle.size() <= kMaxMessageLength);
+
+  int returned_fd = -1;
+  uint8_t reply_buf[kMaxMessageLength];
+  ssize_t msg_len = base::UnixDomainSocket::SendRecvMsg(
+      ipc_channel_.get(), reply_buf, sizeof(reply_buf), &returned_fd,
+      write_pickle);
+
+  if (msg_len <= 0) {
+    if (!quiet_failures_for_tests_)
+      RAW_LOG(ERROR, "Could not make request to broker process");
+    return -ENOMEM;
+  }
+
+  base::Pickle read_pickle(reinterpret_cast<char*>(reply_buf), msg_len);
+  base::PickleIterator iter(read_pickle);
+  int return_value = -1;
+  int return_length = 0;
+  const char* return_data = nullptr;
+  if (!iter.ReadInt(&return_value))
+    return -ENOMEM;
+  if (return_value < 0)
+    return return_value;
+  if (!iter.ReadData(&return_data, &return_length))
+    return -ENOMEM;
+  if (return_length < 0)
+    return -ENOMEM;
+  if (static_cast<size_t>(return_length) > bufsize)
+    return -ENAMETOOLONG;
+  memcpy(buf, return_data, return_length);
+  return return_value;
+}
+
 }  // namespace syscall_broker
 }  // namespace sandbox
diff --git a/sandbox/linux/syscall_broker/broker_client.h b/sandbox/linux/syscall_broker/broker_client.h
index 9c81857..3ad3f25 100644
--- a/sandbox/linux/syscall_broker/broker_client.h
+++ b/sandbox/linux/syscall_broker/broker_client.h
@@ -65,6 +65,9 @@
   // This is async signal safe.
   int Rename(const char* oldpath, const char* newpath);
 
+  // Can be used in place of Readlink().
+  int Readlink(const char* path, char* buf, size_t bufsize);
+
   // Get the file descriptor used for IPC. This is used for tests.
   int GetIPCDescriptor() const { return ipc_channel_.get(); }
 
diff --git a/sandbox/linux/syscall_broker/broker_common.h b/sandbox/linux/syscall_broker/broker_common.h
index 30b2ce19..dbdc327 100644
--- a/sandbox/linux/syscall_broker/broker_common.h
+++ b/sandbox/linux/syscall_broker/broker_common.h
@@ -9,7 +9,6 @@
 #include <stddef.h>
 
 namespace sandbox {
-
 namespace syscall_broker {
 
 const size_t kMaxMessageLength = 4096;
@@ -35,10 +34,10 @@
   COMMAND_STAT,
   COMMAND_STAT64,
   COMMAND_RENAME,
+  COMMAND_READLINK,
 };
 
 }  // namespace syscall_broker
-
 }  // namespace sandbox
 
 #endif  // SANDBOX_LINUX_SYSCALL_BROKER_BROKER_COMMON_H_
diff --git a/sandbox/linux/syscall_broker/broker_host.cc b/sandbox/linux/syscall_broker/broker_host.cc
index 375747e..68a5260 100644
--- a/sandbox/linux/syscall_broker/broker_host.cc
+++ b/sandbox/linux/syscall_broker/broker_host.cc
@@ -6,6 +6,7 @@
 
 #include <errno.h>
 #include <fcntl.h>
+#include <limits.h>
 #include <stddef.h>
 #include <sys/socket.h>
 #include <sys/stat.h>
@@ -110,7 +111,6 @@
                     IPCCommand command_type,
                     const std::string& requested_filename,
                     base::Pickle* write_pickle) {
-  DCHECK(write_pickle);
   DCHECK(command_type == COMMAND_STAT || command_type == COMMAND_STAT64);
   const char* file_to_access = nullptr;
   if (!policy.GetFileNameIfAllowedToAccess(requested_filename.c_str(), F_OK,
@@ -143,7 +143,6 @@
                       const std::string& old_filename,
                       const std::string& new_filename,
                       base::Pickle* write_pickle) {
-  DCHECK(write_pickle);
   bool ignore;
   const char* old_file_to_access = nullptr;
   const char* new_file_to_access = nullptr;
@@ -161,6 +160,27 @@
   write_pickle->WriteInt(0);
 }
 
+// Perform readlink(2) on |filename| using a buffer of MAX_PATH bytes.
+void ReadlinkFileForIPC(const BrokerPolicy& policy,
+                        const std::string& filename,
+                        base::Pickle* write_pickle) {
+  bool ignore;
+  const char* file_to_access = nullptr;
+  if (!policy.GetFileNameIfAllowedToOpen(filename.c_str(), O_RDONLY,
+                                         &file_to_access, &ignore)) {
+    write_pickle->WriteInt(-policy.denied_errno());
+    return;
+  }
+  char buf[PATH_MAX];
+  ssize_t result = readlink(file_to_access, buf, sizeof(buf));
+  if (result < 0) {
+    write_pickle->WriteInt(-errno);
+    return;
+  }
+  write_pickle->WriteInt(result);
+  write_pickle->WriteData(buf, result);
+}
+
 // Handle a |command_type| request contained in |iter| and write the reply
 // to |write_pickle|, adding any files opened to |opened_files|.
 bool HandleRemoteCommand(const BrokerPolicy& policy,
@@ -206,6 +226,13 @@
       RenameFileForIPC(policy, old_filename, new_filename, write_pickle);
       break;
     }
+    case COMMAND_READLINK: {
+      std::string filename;
+      if (!iter.ReadString(&filename))
+        return false;
+      ReadlinkFileForIPC(policy, filename, write_pickle);
+      break;
+    }
     default:
       LOG(ERROR) << "Invalid IPC command";
       return false;
diff --git a/sandbox/linux/syscall_broker/broker_process.cc b/sandbox/linux/syscall_broker/broker_process.cc
index 46e207ed..9bec522 100644
--- a/sandbox/linux/syscall_broker/broker_process.cc
+++ b/sandbox/linux/syscall_broker/broker_process.cc
@@ -129,6 +129,11 @@
   return broker_client_->Rename(oldpath, newpath);
 }
 
+int BrokerProcess::Readlink(const char* path, char* buf, size_t bufsize) const {
+  RAW_CHECK(initialized_);
+  return broker_client_->Readlink(path, buf, bufsize);
+}
+
 #if defined(MEMORY_SANITIZER)
 #define BROKER_UNPOISON_STRING(x) __msan_unpoison_string(x)
 #else
@@ -196,6 +201,22 @@
       return broker_process->Stat(reinterpret_cast<const char*>(args.args[1]),
                                   reinterpret_cast<struct stat*>(args.args[2]));
 #endif
+#if defined(__NR_readlink)
+    case __NR_readlink:
+      return broker_process->Readlink(
+          reinterpret_cast<const char*>(args.args[0]),
+          reinterpret_cast<char*>(args.args[1]),
+          static_cast<size_t>(args.args[2]));
+#endif
+#if defined(__NR_readlinkat)
+    case __NR_readlinkat:
+      if (static_cast<int>(args.args[0]) != AT_FDCWD)
+        return -EPERM;
+      return broker_process->Readlink(
+          reinterpret_cast<const char*>(args.args[1]),
+          reinterpret_cast<char*>(args.args[2]),
+          static_cast<size_t>(args.args[3]));
+#endif
 #if defined(__NR_rename)
     case __NR_rename:
       return broker_process->Rename(
diff --git a/sandbox/linux/syscall_broker/broker_process.h b/sandbox/linux/syscall_broker/broker_process.h
index efa84bb..f9a571d9 100644
--- a/sandbox/linux/syscall_broker/broker_process.h
+++ b/sandbox/linux/syscall_broker/broker_process.h
@@ -83,6 +83,10 @@
   // It's similar to the rename() system call and will return -errno on errors.
   int Rename(const char* oldpath, const char* newpath) const;
 
+  // Can be used in place of readlink(). Will be async signal safe.
+  // It's similar to the read() system call and will return -errno on errors.
+  int Readlink(const char* path, char* buf, size_t bufsize) const;
+
   int broker_pid() const { return broker_pid_; }
 
   // Handler to be used with a bpf_dsl Trap() function to forward system calls
diff --git a/sandbox/linux/syscall_broker/broker_process_unittest.cc b/sandbox/linux/syscall_broker/broker_process_unittest.cc
index a536003..df49a50 100644
--- a/sandbox/linux/syscall_broker/broker_process_unittest.cc
+++ b/sandbox/linux/syscall_broker/broker_process_unittest.cc
@@ -60,7 +60,7 @@
 
   std::unique_ptr<BrokerProcess> open_broker(
       new BrokerProcess(EPERM, permissions));
-  ASSERT_TRUE(open_broker->Init(base::Bind(&NoOpCallback)));
+  ASSERT_TRUE(open_broker->Init(base::BindRepeating(&NoOpCallback)));
 
   ASSERT_TRUE(TestUtils::CurrentProcessHasChildren());
   // Destroy the broker and check it has exited properly.
@@ -71,7 +71,7 @@
 TEST(BrokerProcess, TestOpenAccessNull) {
   std::vector<BrokerFilePermission> empty;
   BrokerProcess open_broker(EPERM, empty);
-  ASSERT_TRUE(open_broker.Init(base::Bind(&NoOpCallback)));
+  ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
 
   int fd = open_broker.Open(NULL, O_RDONLY);
   ASSERT_EQ(fd, -EFAULT);
@@ -97,7 +97,7 @@
   permissions.push_back(BrokerFilePermission::ReadWrite(kRW_WhiteListed));
 
   BrokerProcess open_broker(denied_errno, permissions, fast_check_in_client);
-  ASSERT_TRUE(open_broker.Init(base::Bind(&NoOpCallback)));
+  ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
 
   int fd = -1;
   fd = open_broker.Open(kR_WhiteListed, O_RDONLY);
@@ -254,7 +254,7 @@
   permissions.push_back(BrokerFilePermission::ReadOnlyRecursive("/proc/"));
   std::unique_ptr<BrokerProcess> open_broker(
       new BrokerProcess(EPERM, permissions, fast_check_in_client));
-  ASSERT_TRUE(open_broker->Init(base::Bind(&NoOpCallback)));
+  ASSERT_TRUE(open_broker->Init(base::BindRepeating(&NoOpCallback)));
   // Open cpuinfo via the broker.
   int cpuinfo_fd = open_broker->Open(kFileCpuInfo, O_RDONLY);
   base::ScopedFD cpuinfo_fd_closer(cpuinfo_fd);
@@ -313,7 +313,7 @@
 
   std::unique_ptr<BrokerProcess> open_broker(
       new BrokerProcess(EPERM, permissions, fast_check_in_client));
-  ASSERT_TRUE(open_broker->Init(base::Bind(&NoOpCallback)));
+  ASSERT_TRUE(open_broker->Init(base::BindRepeating(&NoOpCallback)));
 
   int fd = -1;
   fd = open_broker->Open(kFileCpuInfo, O_RDWR);
@@ -390,7 +390,7 @@
   permissions.push_back(BrokerFilePermission::ReadWrite(tempfile_name));
 
   BrokerProcess open_broker(EPERM, permissions);
-  ASSERT_TRUE(open_broker.Init(base::Bind(&NoOpCallback)));
+  ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
 
   // Check we can access that file with read or write.
   int can_access = open_broker.Access(tempfile_name, R_OK | W_OK);
@@ -425,7 +425,7 @@
 
   BrokerProcess open_broker(EPERM, permissions, true /* fast_check_in_client */,
                             true /* quiet_failures_for_tests */);
-  SANDBOX_ASSERT(open_broker.Init(base::Bind(&NoOpCallback)));
+  SANDBOX_ASSERT(open_broker.Init(base::BindRepeating(&NoOpCallback)));
   const pid_t broker_pid = open_broker.broker_pid();
   SANDBOX_ASSERT(kill(broker_pid, SIGKILL) == 0);
 
@@ -449,7 +449,7 @@
   permissions.push_back(BrokerFilePermission::ReadOnly(kCpuInfo));
 
   BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
-  ASSERT_TRUE(open_broker.Init(base::Bind(&NoOpCallback)));
+  ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
   // Test that we do the right thing for O_CLOEXEC and O_NONBLOCK.
   int fd = -1;
   int ret = 0;
@@ -538,7 +538,7 @@
   permissions.push_back(BrokerFilePermission::ReadOnly(kCpuInfo));
 
   BrokerProcess open_broker(EPERM, permissions);
-  SANDBOX_ASSERT(open_broker.Init(base::Bind(&NoOpCallback)));
+  SANDBOX_ASSERT(open_broker.Init(base::BindRepeating(&NoOpCallback)));
 
   const int ipc_fd = BrokerProcessTestHelper::GetIPCDescriptor(&open_broker);
   SANDBOX_ASSERT(ipc_fd >= 0);
@@ -588,7 +588,7 @@
 
   BrokerProcess open_broker(EPERM, permissions, true /* fast_check_in_client */,
                             false /* quiet_failures_for_tests */);
-  ASSERT_TRUE(open_broker.Init(base::Bind(&CloseFD, lifeline_fds[0])));
+  ASSERT_TRUE(open_broker.Init(base::BindRepeating(&CloseFD, lifeline_fds[0])));
   // Make sure the writing end only exists in the broker process.
   CloseFD(lifeline_fds[1]);
   base::ScopedFD reader(lifeline_fds[0]);
@@ -624,7 +624,7 @@
   permissions.push_back(BrokerFilePermission::ReadWriteCreate(tempfile_name));
 
   BrokerProcess open_broker(EPERM, permissions);
-  ASSERT_TRUE(open_broker.Init(base::Bind(&NoOpCallback)));
+  ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
 
   int fd = -1;
 
@@ -674,7 +674,7 @@
     // Nonexistent file with no permissions to see file.
     std::vector<BrokerFilePermission> permissions;
     BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
-    ASSERT_TRUE(open_broker.Init(base::Bind(&NoOpCallback)));
+    ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
 
     memset(&sb, 0, sizeof(sb));
     EXPECT_EQ(-EPERM, open_broker.Stat(nonesuch_name, &sb));
@@ -683,7 +683,7 @@
     // Actual file with no permission to see file.
     std::vector<BrokerFilePermission> permissions;
     BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
-    ASSERT_TRUE(open_broker.Init(base::Bind(&NoOpCallback)));
+    ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
 
     memset(&sb, 0, sizeof(sb));
     EXPECT_EQ(-EPERM, open_broker.Stat(tempfile_name, &sb));
@@ -693,7 +693,7 @@
     std::vector<BrokerFilePermission> permissions;
     permissions.push_back(BrokerFilePermission::ReadOnly(nonesuch_name));
     BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
-    ASSERT_TRUE(open_broker.Init(base::Bind(&NoOpCallback)));
+    ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
 
     memset(&sb, 0, sizeof(sb));
     EXPECT_EQ(-ENOENT, open_broker.Stat(nonesuch_name, &sb));
@@ -703,7 +703,7 @@
     std::vector<BrokerFilePermission> permissions;
     permissions.push_back(BrokerFilePermission::ReadOnly(tempfile_name));
     BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
-    ASSERT_TRUE(open_broker.Init(base::Bind(&NoOpCallback)));
+    ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
 
     memset(&sb, 0, sizeof(sb));
     EXPECT_EQ(0, open_broker.Stat(tempfile_name, &sb));
@@ -754,7 +754,7 @@
 
     bool fast_check_in_client = false;
     BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
-    ASSERT_TRUE(open_broker.Init(base::Bind(&NoOpCallback)));
+    ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
     EXPECT_EQ(-EPERM, open_broker.Rename(oldpath.c_str(), newpath.c_str()));
 
     // ... and no files moved around.
@@ -768,7 +768,7 @@
 
     bool fast_check_in_client = false;
     BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
-    ASSERT_TRUE(open_broker.Init(base::Bind(&NoOpCallback)));
+    ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
     EXPECT_EQ(-EPERM, open_broker.Rename(oldpath.c_str(), newpath.c_str()));
 
     // ... and no files moved around.
@@ -783,7 +783,7 @@
 
     bool fast_check_in_client = false;
     BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
-    ASSERT_TRUE(open_broker.Init(base::Bind(&NoOpCallback)));
+    ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
     EXPECT_EQ(-EPERM, open_broker.Rename(oldpath.c_str(), newpath.c_str()));
 
     // ... and no files moved around.
@@ -798,7 +798,7 @@
 
     bool fast_check_in_client = false;
     BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
-    ASSERT_TRUE(open_broker.Init(base::Bind(&NoOpCallback)));
+    ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
     EXPECT_EQ(-EPERM, open_broker.Rename(oldpath.c_str(), newpath.c_str()));
 
     // ... and no files moved around.
@@ -813,7 +813,7 @@
 
     bool fast_check_in_client = false;
     BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
-    ASSERT_TRUE(open_broker.Init(base::Bind(&NoOpCallback)));
+    ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
     EXPECT_EQ(0, open_broker.Rename(oldpath.c_str(), newpath.c_str()));
 
     // ... and files were moved around.
@@ -825,5 +825,77 @@
   unlink(newpath.c_str());
 }
 
+void TestReadlinkHelper(bool fast_check_in_client) {
+  std::string oldpath;
+  std::string newpath;
+  {
+    // Just to generate names and ensure they do not exist upon scope exit.
+    ScopedTemporaryFile oldfile;
+    ScopedTemporaryFile newfile;
+    oldpath = oldfile.full_file_name();
+    newpath = newfile.full_file_name();
+  }
+
+  // Now make a link from old to new path name.
+  EXPECT_TRUE(symlink(oldpath.c_str(), newpath.c_str()) == 0);
+
+  const char* nonesuch_name = "/mbogo/nonesuch";
+  const char* oldpath_name = oldpath.c_str();
+  const char* newpath_name = newpath.c_str();
+  char buf[1024];
+  {
+    // Nonexistent file with no permissions to see file.
+    std::vector<BrokerFilePermission> permissions;
+    BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
+    ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
+    EXPECT_EQ(-EPERM, open_broker.Readlink(nonesuch_name, buf, sizeof(buf)));
+  }
+  {
+    // Actual file with no permissions to see file.
+    std::vector<BrokerFilePermission> permissions;
+    BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
+    ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
+    EXPECT_EQ(-EPERM, open_broker.Readlink(newpath_name, buf, sizeof(buf)));
+  }
+  {
+    // Nonexistent file with permissions to see file.
+    std::vector<BrokerFilePermission> permissions;
+    permissions.push_back(BrokerFilePermission::ReadOnly(nonesuch_name));
+    BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
+    ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
+    EXPECT_EQ(-ENOENT, open_broker.Readlink(nonesuch_name, buf, sizeof(buf)));
+  }
+  {
+    // Actual file with permissions to see file.
+    std::vector<BrokerFilePermission> permissions;
+    permissions.push_back(BrokerFilePermission::ReadOnly(newpath_name));
+    BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
+    ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
+    ssize_t retlen = open_broker.Readlink(newpath_name, buf, sizeof(buf));
+    EXPECT_TRUE(retlen == static_cast<ssize_t>(strlen(oldpath_name)));
+    EXPECT_EQ(0, memcmp(oldpath_name, buf, retlen));
+  }
+  {
+    // Actual file with permissions to see file, but too small a buffer.
+    std::vector<BrokerFilePermission> permissions;
+    permissions.push_back(BrokerFilePermission::ReadOnly(newpath_name));
+    BrokerProcess open_broker(EPERM, permissions, fast_check_in_client);
+    ASSERT_TRUE(open_broker.Init(base::BindRepeating(&NoOpCallback)));
+    EXPECT_EQ(-ENAMETOOLONG, open_broker.Readlink(newpath_name, buf, 4));
+  }
+
+  // Cleanup both paths.
+  unlink(oldpath.c_str());
+  unlink(newpath.c_str());
+}
+
+TEST(BrokerProcess, ReadlinkFileClient) {
+  TestReadlinkHelper(true);
+}
+
+TEST(BrokerProcess, ReadlinkFileHost) {
+  TestReadlinkHelper(false);
+}
+
 }  // namespace syscall_broker
 }  // namespace sandbox
diff --git a/testing/buildbot/filters/OWNERS b/testing/buildbot/filters/OWNERS
index 83f9ffc..42c55bb 100644
--- a/testing/buildbot/filters/OWNERS
+++ b/testing/buildbot/filters/OWNERS
@@ -1,15 +1,4 @@
 *
-# PlzNavigate: please check with team before disabling any tests.
-per-file browser-side-navigation*=set noparent
-per-file browser-side-navigation*=ananta@chromium.org
-per-file browser-side-navigation*=arthursonzogni@chromium.org
-per-file browser-side-navigation*=clamy@chromium.org
-per-file browser-side-navigation*=creis@chromium.org
-per-file browser-side-navigation*=jam@chromium.org
-per-file browser-side-navigation*=nasko@chromium.org
-per-file browser-side-navigation*=scottmg@chromium.org
-per-file browser-side-navigation*=yzshen@chromium.org
-
 per-file fuchsia*=file://build/fuchsia/OWNERS
 
 # TEAM: infra-dev@chromium.org
diff --git a/testing/libfuzzer/fuzzers/v8_fuzzer.cc b/testing/libfuzzer/fuzzers/v8_fuzzer.cc
index 89e447c..6908515 100644
--- a/testing/libfuzzer/fuzzers/v8_fuzzer.cc
+++ b/testing/libfuzzer/fuzzers/v8_fuzzer.cc
@@ -27,7 +27,7 @@
 // kSleepSeconds + kMaxExecutionSeconds.
 // TODO(metzman): Determine if having such a short timeout causes too much
 // indeterminism.
-static const seconds kMaxExecutionSeconds(12);
+static const seconds kMaxExecutionSeconds(7);
 
 // Inspired by/copied from d8 code, this allocator will return nullptr when
 // an allocation request is made that puts currently_allocated_ over
@@ -153,11 +153,11 @@
 }
 
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  static Environment* env = new Environment();
+
   if (size < 1)
     return 0;
 
-  static Environment* env = new Environment();
-
   v8::Isolate::Scope isolate_scope(env->isolate);
   v8::HandleScope handle_scope(env->isolate);
   v8::Local<v8::Context> context = v8::Context::New(env->isolate);
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index a3a4fa4..dbde41f 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -3257,6 +3257,37 @@
             ]
         }
     ],
+    "SavePreviousDocumentResources": [
+        {
+            "platforms": [
+                "android",
+                "chromeos",
+                "linux",
+                "mac",
+                "win"
+            ],
+            "experiments": [
+                {
+                    "name": "until-dcl",
+                    "params": {
+                        "until": "onDOMContentLoaded"
+                    },
+                    "enable_features": [
+                        "SavePreviousDocumentResources"
+                    ]
+                },
+                {
+                    "name": "until-onload",
+                    "params": {
+                        "until": "onload"
+                    },
+                    "enable_features": [
+                        "SavePreviousDocumentResources"
+                    ]
+                }
+            ]
+        }
+    ],
     "SearchEnginePromo": [
         {
             "platforms": [
diff --git a/third_party/WebKit/LayoutTests/FlagExpectations/root-layer-scrolls b/third_party/WebKit/LayoutTests/FlagExpectations/root-layer-scrolls
index 531662d..7627acd 100644
--- a/third_party/WebKit/LayoutTests/FlagExpectations/root-layer-scrolls
+++ b/third_party/WebKit/LayoutTests/FlagExpectations/root-layer-scrolls
@@ -47,12 +47,6 @@
 crbug.com/417782 inspector-protocol/dom-snapshot/dom-snapshot-getSnapshot.js [ Failure ]
 crbug.com/417782 inspector-protocol/layers/paint-profiler.js [ Crash ]
 crbug.com/417782 inspector-protocol/page/get-layout-metrics.js [ Failure ]
-crbug.com/417782 paint/invalidation/resize-iframe-text.html [ Failure ]
-crbug.com/417782 paint/invalidation/scroll/overflow-scroll-body-appear.html [ Failure ]
-crbug.com/417782 paint/invalidation/svg/absolute-sized-document-no-scrollbars.svg [ Failure ]
-crbug.com/417782 paint/invalidation/svg/deep-nested-embedded-svg-size-changes-no-layout-triggers-1.html [ Failure ]
-crbug.com/417782 paint/invalidation/svg/deep-nested-embedded-svg-size-changes-no-layout-triggers-2.html [ Failure ]
-crbug.com/417782 paint/invalidation/svg/window.svg [ Failure ]
 crbug.com/417782 paint/invalidation/window-resize/window-resize-vertical-writing-mode.html [ Crash ]
 crbug.com/417782 [ Mac Win ] paint/overflow/fixed-background-scroll-window.html [ Failure ]
 crbug.com/417782 plugins/webview-plugin-nested-iframe-scroll.html [ Failure ]
diff --git a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/paint/invalidation/svg/absolute-sized-document-no-scrollbars-expected.txt b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/paint/invalidation/svg/absolute-sized-document-no-scrollbars-expected.txt
index edd7fe5..2aae581 100644
--- a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/paint/invalidation/svg/absolute-sized-document-no-scrollbars-expected.txt
+++ b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/paint/invalidation/svg/absolute-sized-document-no-scrollbars-expected.txt
@@ -3,7 +3,14 @@
     {
       "name": "LayoutView #document",
       "bounds": [800, 600],
-      "backgroundColor": "#FFFFFF"
+      "backgroundColor": "#FFFFFF",
+      "paintInvalidations": [
+        {
+          "object": "LayoutView #document",
+          "rect": [0, 0, 800, 600],
+          "reason": "style change"
+        }
+      ]
     },
     {
       "name": "Scrolling Layer",
@@ -17,6 +24,11 @@
       "backgroundColor": "#FFFFFF",
       "paintInvalidations": [
         {
+          "object": "LayoutView #document",
+          "rect": [0, 0, 800, 600],
+          "reason": "background on scrolling contents layer"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [0, 0, 576, 432],
           "reason": "style change"
@@ -36,6 +48,14 @@
   ],
   "objectPaintInvalidations": [
     {
+      "object": "Scrolling Contents Layer",
+      "reason": "background on scrolling contents layer"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "style change"
+    },
+    {
       "object": "LayoutSVGRoot svg",
       "reason": "style change"
     },
diff --git a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/paint/invalidation/svg/deep-nested-embedded-svg-size-changes-no-layout-triggers-1-expected.txt b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/paint/invalidation/svg/deep-nested-embedded-svg-size-changes-no-layout-triggers-1-expected.txt
new file mode 100644
index 0000000..3f738b5
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/paint/invalidation/svg/deep-nested-embedded-svg-size-changes-no-layout-triggers-1-expected.txt
@@ -0,0 +1,73 @@
+{
+  "objectPaintInvalidations": [
+    {
+      "object": "LayoutBlockFlow BODY",
+      "reason": "geometry"
+    },
+    {
+      "object": "RootInlineBox",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutEmbeddedObject OBJECT",
+      "reason": "style change"
+    },
+    {
+      "object": "HorizontalScrollbar",
+      "reason": "scroll control"
+    },
+    {
+      "object": "VerticalScrollbar",
+      "reason": "scroll control"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutBlockFlow HTML",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutBlockFlow BODY",
+      "reason": "geometry"
+    },
+    {
+      "object": "RootInlineBox",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutEmbeddedObject OBJECT",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutText #text",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutSVGRoot svg",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutSVGRect rect",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutSVGRect rect",
+      "reason": "geometry"
+    }
+  ]
+}
+
diff --git a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/paint/invalidation/svg/deep-nested-embedded-svg-size-changes-no-layout-triggers-2-expected.txt b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/paint/invalidation/svg/deep-nested-embedded-svg-size-changes-no-layout-triggers-2-expected.txt
new file mode 100644
index 0000000..3f738b5
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/paint/invalidation/svg/deep-nested-embedded-svg-size-changes-no-layout-triggers-2-expected.txt
@@ -0,0 +1,73 @@
+{
+  "objectPaintInvalidations": [
+    {
+      "object": "LayoutBlockFlow BODY",
+      "reason": "geometry"
+    },
+    {
+      "object": "RootInlineBox",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutEmbeddedObject OBJECT",
+      "reason": "style change"
+    },
+    {
+      "object": "HorizontalScrollbar",
+      "reason": "scroll control"
+    },
+    {
+      "object": "VerticalScrollbar",
+      "reason": "scroll control"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutBlockFlow HTML",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutBlockFlow BODY",
+      "reason": "geometry"
+    },
+    {
+      "object": "RootInlineBox",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutEmbeddedObject OBJECT",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutText #text",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutSVGRoot svg",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutSVGRect rect",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutSVGRect rect",
+      "reason": "geometry"
+    }
+  ]
+}
+
diff --git a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/linux/paint/invalidation/scroll/overflow-scroll-body-appear-expected.txt b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/linux/paint/invalidation/scroll/overflow-scroll-body-appear-expected.txt
index 3801109..f36dc547 100644
--- a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/linux/paint/invalidation/scroll/overflow-scroll-body-appear-expected.txt
+++ b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/linux/paint/invalidation/scroll/overflow-scroll-body-appear-expected.txt
@@ -7,6 +7,11 @@
       "paintInvalidations": [
         {
           "object": "LayoutView #document",
+          "rect": [0, 0, 800, 600],
+          "reason": "style change"
+        },
+        {
+          "object": "LayoutView #document",
           "rect": [0, 585, 785, 15],
           "reason": "scroll control"
         },
@@ -74,6 +79,10 @@
   "objectPaintInvalidations": [
     {
       "object": "LayoutView #document",
+      "reason": "style change"
+    },
+    {
+      "object": "LayoutView #document",
       "reason": "geometry"
     },
     {
diff --git a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/linux/paint/invalidation/svg/window-expected.txt b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/linux/paint/invalidation/svg/window-expected.txt
index e0ae16f..f43ad6f5 100644
--- a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/linux/paint/invalidation/svg/window-expected.txt
+++ b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/linux/paint/invalidation/svg/window-expected.txt
@@ -518,7 +518,7 @@
         },
         {
           "object": "LayoutSVGContainer g id='windowTitlebarGroupnavWindow'",
-          "rect": [755, 93, 39, 13],
+          "rect": [756, 94, 37, 11],
           "reason": "appeared"
         },
         {
@@ -537,91 +537,6 @@
           "reason": "appeared"
         },
         {
-          "object": "LayoutSVGPath line",
-          "rect": [614, 82, 14, 14],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [614, 82, 14, 14],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [453, 378, 14, 14],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [453, 378, 14, 14],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [234, 196, 14, 14],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [234, 196, 14, 14],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [781, 93, 14, 13],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [781, 93, 14, 13],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [781, 93, 14, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [781, 93, 14, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [570, 144, 14, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [570, 144, 14, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [754, 101, 14, 5],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [754, 101, 14, 5],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [614, 122, 14, 5],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [426, 387, 14, 5],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [348, 191, 14, 5],
-          "reason": "appeared"
-        },
-        {
           "object": "LayoutSVGInlineText #text",
           "rect": [615, 153, 13, 81],
           "reason": "appeared"
@@ -632,169 +547,34 @@
           "reason": "appeared"
         },
         {
-          "object": "LayoutSVGContainer use id='closeButtonsmallWindow'",
-          "rect": [453, 378, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [453, 378, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonnestedWindow'",
-          "rect": [375, 183, 13, 13],
-          "reason": "appeared"
-        },
-        {
           "object": "LayoutSVGPath line",
-          "rect": [375, 183, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [375, 183, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [375, 183, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonstatusWindow'",
-          "rect": [362, 346, 13, 12],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [362, 346, 13, 12],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
-          "rect": [755, 95, 13, 11],
-          "reason": "full"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
-          "rect": [755, 95, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [755, 95, 13, 11],
-          "reason": "full"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [755, 95, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonbigWindow'",
-          "rect": [544, 146, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [544, 146, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonsmallWindow'",
-          "rect": [427, 380, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [427, 380, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtoncolourPickerWindow'",
-          "rect": [208, 198, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [208, 198, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [208, 204, 13, 6],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [544, 152, 13, 5],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [362, 353, 13, 5],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
-          "rect": [782, 93, 12, 13],
-          "reason": "full"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
-          "rect": [782, 93, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [782, 93, 12, 13],
-          "reason": "full"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [782, 93, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonbigWindow'",
-          "rect": [571, 144, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [571, 144, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtoncolourPickerWindow'",
-          "rect": [235, 196, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [235, 196, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
           "rect": [615, 83, 12, 12],
-          "reason": "full"
+          "reason": "geometry"
         },
         {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "object": "LayoutSVGPath line",
           "rect": [615, 83, 12, 12],
-          "reason": "full"
+          "reason": "geometry"
         },
         {
-          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
-          "rect": [615, 116, 12, 11],
-          "reason": "full"
+          "object": "LayoutSVGPath line",
+          "rect": [782, 94, 12, 11],
+          "reason": "geometry"
         },
         {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [615, 116, 12, 11],
-          "reason": "full"
+          "object": "LayoutSVGPath line",
+          "rect": [782, 94, 12, 11],
+          "reason": "geometry"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [782, 94, 12, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [782, 94, 12, 11],
+          "reason": "appeared"
         },
         {
           "object": "LayoutSVGRect rect",
@@ -802,13 +582,33 @@
           "reason": "geometry"
         },
         {
-          "object": "LayoutSVGContainer use id='minimizeButtonnestedWindow'",
-          "rect": [349, 185, 12, 11],
+          "object": "LayoutSVGPath line",
+          "rect": [571, 145, 12, 11],
           "reason": "appeared"
         },
         {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [349, 185, 12, 11],
+          "object": "LayoutSVGPath line",
+          "rect": [571, 145, 12, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [235, 197, 12, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [235, 197, 12, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [615, 123, 12, 3],
+          "reason": "geometry"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [349, 192, 12, 3],
           "reason": "appeared"
         },
         {
@@ -862,11 +662,41 @@
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGPath line",
+          "rect": [454, 379, 11, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [454, 379, 11, 11],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [441, 379, 11, 11],
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGContainer use id='closeButtonnestedWindow'",
+          "rect": [376, 184, 11, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [376, 184, 11, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [376, 184, 11, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [376, 184, 11, 11],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGContainer use id='maximizeButtonnestedWindow'",
           "rect": [363, 184, 11, 11],
           "reason": "appeared"
@@ -887,6 +717,36 @@
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
+          "rect": [756, 95, 11, 10],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
+          "rect": [756, 95, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [756, 95, 11, 10],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [756, 95, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtonbigWindow'",
+          "rect": [545, 146, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [545, 146, 11, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGContainer use id='maximizeButtonsmallWindow'",
           "rect": [441, 380, 11, 10],
           "reason": "appeared"
@@ -907,6 +767,86 @@
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGContainer use id='minimizeButtonstatusWindow'",
+          "rect": [363, 346, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [363, 346, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtoncolourPickerWindow'",
+          "rect": [209, 198, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [209, 198, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [545, 153, 11, 3],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [363, 354, 11, 3],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [756, 103, 11, 2],
+          "reason": "geometry"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [756, 103, 11, 2],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [428, 388, 11, 2],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [209, 206, 11, 2],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
+          "rect": [783, 94, 10, 11],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
+          "rect": [783, 94, 10, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [783, 94, 10, 11],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [783, 94, 10, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtonbigWindow'",
+          "rect": [572, 145, 10, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [572, 145, 10, 11],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGContainer use id='maximizeButtonnavWindow'",
           "rect": [616, 100, 10, 10],
           "reason": "full"
@@ -917,26 +857,76 @@
           "reason": "full"
         },
         {
+          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
+          "rect": [616, 84, 10, 10],
+          "reason": "full"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [616, 84, 10, 10],
           "reason": "geometry"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [616, 84, 10, 10],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtonsmallWindow'",
+          "rect": [455, 380, 10, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [455, 380, 10, 10],
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [455, 380, 10, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtonsmallWindow'",
+          "rect": [428, 380, 10, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [428, 380, 10, 10],
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [428, 380, 10, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtonnestedWindow'",
+          "rect": [350, 185, 10, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [350, 185, 10, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtoncolourPickerWindow'",
+          "rect": [236, 198, 10, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [236, 198, 10, 10],
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [236, 198, 10, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGContainer use id='maximizeButtoncolourPickerWindow'",
           "rect": [223, 198, 10, 10],
           "reason": "appeared"
@@ -957,11 +947,21 @@
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
+          "rect": [616, 116, 10, 9],
+          "reason": "full"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [616, 116, 10, 9],
           "reason": "geometry"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [616, 116, 10, 9],
+          "reason": "full"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [572, 146, 10, 9],
           "reason": "appeared"
diff --git a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/mac/paint/invalidation/resize-iframe-text-expected.txt b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/mac/paint/invalidation/resize-iframe-text-expected.txt
new file mode 100644
index 0000000..338f2b9
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/mac/paint/invalidation/resize-iframe-text-expected.txt
@@ -0,0 +1,89 @@
+{
+  "layers": [
+    {
+      "name": "LayoutView #document",
+      "bounds": [500, 400],
+      "backgroundColor": "#FFFFFF",
+      "paintInvalidations": [
+        {
+          "object": "LayoutView #document",
+          "rect": [0, 200, 500, 200],
+          "reason": "incremental"
+        }
+      ]
+    },
+    {
+      "name": "Scrolling Layer",
+      "bounds": [500, 400],
+      "drawsContent": false
+    },
+    {
+      "name": "Scrolling Contents Layer",
+      "bounds": [500, 400],
+      "contentsOpaque": true,
+      "backgroundColor": "#FFFFFF",
+      "paintInvalidations": [
+        {
+          "object": "LayoutIFrame (positioned) IFRAME",
+          "rect": [0, 200, 500, 200],
+          "reason": "incremental"
+        },
+        {
+          "object": "LayoutView #document",
+          "rect": [0, 200, 500, 200],
+          "reason": "incremental"
+        },
+        {
+          "object": "LayoutView #document",
+          "rect": [0, 200, 500, 200],
+          "reason": "background on scrolling contents layer"
+        },
+        {
+          "object": "LayoutBlockFlow H3",
+          "rect": [8, 300, 400, 22],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutView #document",
+          "rect": [485, 0, 15, 200],
+          "reason": "scroll control"
+        }
+      ]
+    }
+  ],
+  "objectPaintInvalidations": [
+    {
+      "object": "Scrolling Contents Layer",
+      "reason": "background on scrolling contents layer"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "incremental"
+    },
+    {
+      "object": "LayoutIFrame (positioned) IFRAME",
+      "reason": "incremental"
+    },
+    {
+      "object": "VerticalScrollbar",
+      "reason": "scroll control"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "incremental"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutBlockFlow H3",
+      "reason": "appeared"
+    },
+    {
+      "object": "RootInlineBox",
+      "reason": "appeared"
+    }
+  ]
+}
+
diff --git a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/mac/paint/invalidation/scroll/overflow-scroll-body-appear-expected.txt b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/mac/paint/invalidation/scroll/overflow-scroll-body-appear-expected.txt
index 5f80b67..0e0fb86 100644
--- a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/mac/paint/invalidation/scroll/overflow-scroll-body-appear-expected.txt
+++ b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/mac/paint/invalidation/scroll/overflow-scroll-body-appear-expected.txt
@@ -7,6 +7,11 @@
       "paintInvalidations": [
         {
           "object": "LayoutView #document",
+          "rect": [0, 0, 800, 600],
+          "reason": "style change"
+        },
+        {
+          "object": "LayoutView #document",
           "rect": [0, 585, 785, 15],
           "reason": "scroll control"
         },
@@ -74,6 +79,10 @@
   "objectPaintInvalidations": [
     {
       "object": "LayoutView #document",
+      "reason": "style change"
+    },
+    {
+      "object": "LayoutView #document",
       "reason": "geometry"
     },
     {
diff --git a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/mac/paint/invalidation/svg/window-expected.txt b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/mac/paint/invalidation/svg/window-expected.txt
index bfccfe72..af0c4d6 100644
--- a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/mac/paint/invalidation/svg/window-expected.txt
+++ b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/mac/paint/invalidation/svg/window-expected.txt
@@ -518,7 +518,7 @@
         },
         {
           "object": "LayoutSVGContainer g id='windowTitlebarGroupnavWindow'",
-          "rect": [755, 93, 39, 13],
+          "rect": [756, 94, 37, 11],
           "reason": "appeared"
         },
         {
@@ -538,253 +538,33 @@
         },
         {
           "object": "LayoutSVGPath line",
-          "rect": [614, 82, 14, 14],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [614, 82, 14, 14],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [453, 378, 14, 14],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [453, 378, 14, 14],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [234, 196, 14, 14],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [234, 196, 14, 14],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [781, 93, 14, 13],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [781, 93, 14, 13],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [781, 93, 14, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [781, 93, 14, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [570, 144, 14, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [570, 144, 14, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [754, 101, 14, 5],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [754, 101, 14, 5],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [614, 122, 14, 5],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [426, 387, 14, 5],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [348, 191, 14, 5],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonsmallWindow'",
-          "rect": [453, 378, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [453, 378, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonnestedWindow'",
-          "rect": [375, 183, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [375, 183, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [375, 183, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [375, 183, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonstatusWindow'",
-          "rect": [362, 346, 13, 12],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [362, 346, 13, 12],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
-          "rect": [755, 95, 13, 11],
-          "reason": "full"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
-          "rect": [755, 95, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [755, 95, 13, 11],
-          "reason": "full"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [755, 95, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonbigWindow'",
-          "rect": [544, 146, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [544, 146, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonsmallWindow'",
-          "rect": [427, 380, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [427, 380, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtoncolourPickerWindow'",
-          "rect": [208, 198, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [208, 198, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [208, 204, 13, 6],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [544, 152, 13, 5],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [362, 353, 13, 5],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
-          "rect": [782, 93, 12, 13],
-          "reason": "full"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
-          "rect": [782, 93, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [782, 93, 12, 13],
-          "reason": "full"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [782, 93, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonbigWindow'",
-          "rect": [571, 144, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [571, 144, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtoncolourPickerWindow'",
-          "rect": [235, 196, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [235, 196, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
           "rect": [615, 83, 12, 12],
-          "reason": "full"
+          "reason": "geometry"
         },
         {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "object": "LayoutSVGPath line",
           "rect": [615, 83, 12, 12],
-          "reason": "full"
+          "reason": "geometry"
         },
         {
-          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
-          "rect": [615, 116, 12, 11],
-          "reason": "full"
+          "object": "LayoutSVGPath line",
+          "rect": [782, 94, 12, 11],
+          "reason": "geometry"
         },
         {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [615, 116, 12, 11],
-          "reason": "full"
+          "object": "LayoutSVGPath line",
+          "rect": [782, 94, 12, 11],
+          "reason": "geometry"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [782, 94, 12, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [782, 94, 12, 11],
+          "reason": "appeared"
         },
         {
           "object": "LayoutSVGRect rect",
@@ -792,13 +572,33 @@
           "reason": "geometry"
         },
         {
-          "object": "LayoutSVGContainer use id='minimizeButtonnestedWindow'",
-          "rect": [349, 185, 12, 11],
+          "object": "LayoutSVGPath line",
+          "rect": [571, 145, 12, 11],
           "reason": "appeared"
         },
         {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [349, 185, 12, 11],
+          "object": "LayoutSVGPath line",
+          "rect": [571, 145, 12, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [235, 197, 12, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [235, 197, 12, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [615, 123, 12, 3],
+          "reason": "geometry"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [349, 192, 12, 3],
           "reason": "appeared"
         },
         {
@@ -862,11 +662,41 @@
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGPath line",
+          "rect": [454, 379, 11, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [454, 379, 11, 11],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [441, 379, 11, 11],
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGContainer use id='closeButtonnestedWindow'",
+          "rect": [376, 184, 11, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [376, 184, 11, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [376, 184, 11, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [376, 184, 11, 11],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGContainer use id='maximizeButtonnestedWindow'",
           "rect": [363, 184, 11, 11],
           "reason": "appeared"
@@ -887,6 +717,36 @@
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
+          "rect": [756, 95, 11, 10],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
+          "rect": [756, 95, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [756, 95, 11, 10],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [756, 95, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtonbigWindow'",
+          "rect": [545, 146, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [545, 146, 11, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGContainer use id='maximizeButtonsmallWindow'",
           "rect": [441, 380, 11, 10],
           "reason": "appeared"
@@ -907,6 +767,86 @@
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGContainer use id='minimizeButtonstatusWindow'",
+          "rect": [363, 346, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [363, 346, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtoncolourPickerWindow'",
+          "rect": [209, 198, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [209, 198, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [545, 153, 11, 3],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [363, 354, 11, 3],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [756, 103, 11, 2],
+          "reason": "geometry"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [756, 103, 11, 2],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [428, 388, 11, 2],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [209, 206, 11, 2],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
+          "rect": [783, 94, 10, 11],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
+          "rect": [783, 94, 10, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [783, 94, 10, 11],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [783, 94, 10, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtonbigWindow'",
+          "rect": [572, 145, 10, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [572, 145, 10, 11],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGContainer use id='maximizeButtonnavWindow'",
           "rect": [616, 100, 10, 10],
           "reason": "full"
@@ -917,26 +857,76 @@
           "reason": "full"
         },
         {
+          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
+          "rect": [616, 84, 10, 10],
+          "reason": "full"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [616, 84, 10, 10],
           "reason": "geometry"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [616, 84, 10, 10],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtonsmallWindow'",
+          "rect": [455, 380, 10, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [455, 380, 10, 10],
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [455, 380, 10, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtonsmallWindow'",
+          "rect": [428, 380, 10, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [428, 380, 10, 10],
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [428, 380, 10, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtonnestedWindow'",
+          "rect": [350, 185, 10, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [350, 185, 10, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtoncolourPickerWindow'",
+          "rect": [236, 198, 10, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [236, 198, 10, 10],
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [236, 198, 10, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGContainer use id='maximizeButtoncolourPickerWindow'",
           "rect": [223, 198, 10, 10],
           "reason": "appeared"
@@ -957,11 +947,21 @@
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
+          "rect": [616, 116, 10, 9],
+          "reason": "full"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [616, 116, 10, 9],
           "reason": "geometry"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [616, 116, 10, 9],
+          "reason": "full"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [572, 146, 10, 9],
           "reason": "appeared"
diff --git a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/win/paint/invalidation/resize-iframe-text-expected.txt b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/win/paint/invalidation/resize-iframe-text-expected.txt
new file mode 100644
index 0000000..d0603a7
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/win/paint/invalidation/resize-iframe-text-expected.txt
@@ -0,0 +1,89 @@
+{
+  "layers": [
+    {
+      "name": "LayoutView #document",
+      "bounds": [500, 400],
+      "backgroundColor": "#FFFFFF",
+      "paintInvalidations": [
+        {
+          "object": "LayoutView #document",
+          "rect": [0, 200, 500, 200],
+          "reason": "incremental"
+        }
+      ]
+    },
+    {
+      "name": "Scrolling Layer",
+      "bounds": [500, 400],
+      "drawsContent": false
+    },
+    {
+      "name": "Scrolling Contents Layer",
+      "bounds": [500, 400],
+      "contentsOpaque": true,
+      "backgroundColor": "#FFFFFF",
+      "paintInvalidations": [
+        {
+          "object": "LayoutIFrame (positioned) IFRAME",
+          "rect": [0, 200, 500, 200],
+          "reason": "incremental"
+        },
+        {
+          "object": "LayoutView #document",
+          "rect": [0, 200, 500, 200],
+          "reason": "incremental"
+        },
+        {
+          "object": "LayoutView #document",
+          "rect": [0, 200, 500, 200],
+          "reason": "background on scrolling contents layer"
+        },
+        {
+          "object": "LayoutBlockFlow H3",
+          "rect": [8, 300, 400, 23],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutView #document",
+          "rect": [485, 0, 15, 200],
+          "reason": "scroll control"
+        }
+      ]
+    }
+  ],
+  "objectPaintInvalidations": [
+    {
+      "object": "Scrolling Contents Layer",
+      "reason": "background on scrolling contents layer"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "incremental"
+    },
+    {
+      "object": "LayoutIFrame (positioned) IFRAME",
+      "reason": "incremental"
+    },
+    {
+      "object": "VerticalScrollbar",
+      "reason": "scroll control"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "incremental"
+    },
+    {
+      "object": "LayoutView #document",
+      "reason": "geometry"
+    },
+    {
+      "object": "LayoutBlockFlow H3",
+      "reason": "appeared"
+    },
+    {
+      "object": "RootInlineBox",
+      "reason": "appeared"
+    }
+  ]
+}
+
diff --git a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/win/paint/invalidation/scroll/overflow-scroll-body-appear-expected.txt b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/win/paint/invalidation/scroll/overflow-scroll-body-appear-expected.txt
index 884e17e8..3d0f5d1 100644
--- a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/win/paint/invalidation/scroll/overflow-scroll-body-appear-expected.txt
+++ b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/win/paint/invalidation/scroll/overflow-scroll-body-appear-expected.txt
@@ -7,6 +7,11 @@
       "paintInvalidations": [
         {
           "object": "LayoutView #document",
+          "rect": [0, 0, 800, 600],
+          "reason": "style change"
+        },
+        {
+          "object": "LayoutView #document",
           "rect": [0, 585, 785, 15],
           "reason": "scroll control"
         },
@@ -74,6 +79,10 @@
   "objectPaintInvalidations": [
     {
       "object": "LayoutView #document",
+      "reason": "style change"
+    },
+    {
+      "object": "LayoutView #document",
       "reason": "geometry"
     },
     {
diff --git a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/win/paint/invalidation/svg/window-expected.txt b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/win/paint/invalidation/svg/window-expected.txt
index 0552b50f..f20edaa 100644
--- a/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/win/paint/invalidation/svg/window-expected.txt
+++ b/third_party/WebKit/LayoutTests/flag-specific/root-layer-scrolls/platform/win/paint/invalidation/svg/window-expected.txt
@@ -518,7 +518,7 @@
         },
         {
           "object": "LayoutSVGContainer g id='windowTitlebarGroupnavWindow'",
-          "rect": [755, 93, 39, 13],
+          "rect": [756, 94, 37, 11],
           "reason": "appeared"
         },
         {
@@ -537,91 +537,6 @@
           "reason": "appeared"
         },
         {
-          "object": "LayoutSVGPath line",
-          "rect": [614, 82, 14, 14],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [614, 82, 14, 14],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [453, 378, 14, 14],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [453, 378, 14, 14],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [234, 196, 14, 14],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [234, 196, 14, 14],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [781, 93, 14, 13],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [781, 93, 14, 13],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [781, 93, 14, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [781, 93, 14, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [570, 144, 14, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [570, 144, 14, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [754, 101, 14, 5],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [754, 101, 14, 5],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [614, 122, 14, 5],
-          "reason": "geometry"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [426, 387, 14, 5],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [348, 191, 14, 5],
-          "reason": "appeared"
-        },
-        {
           "object": "LayoutSVGInlineText #text",
           "rect": [615, 153, 13, 81],
           "reason": "appeared"
@@ -632,169 +547,34 @@
           "reason": "appeared"
         },
         {
-          "object": "LayoutSVGContainer use id='closeButtonsmallWindow'",
-          "rect": [453, 378, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [453, 378, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonnestedWindow'",
-          "rect": [375, 183, 13, 13],
-          "reason": "appeared"
-        },
-        {
           "object": "LayoutSVGPath line",
-          "rect": [375, 183, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [375, 183, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [375, 183, 13, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonstatusWindow'",
-          "rect": [362, 346, 13, 12],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [362, 346, 13, 12],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
-          "rect": [755, 95, 13, 11],
-          "reason": "full"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
-          "rect": [755, 95, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [755, 95, 13, 11],
-          "reason": "full"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [755, 95, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonbigWindow'",
-          "rect": [544, 146, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [544, 146, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtonsmallWindow'",
-          "rect": [427, 380, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [427, 380, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='minimizeButtoncolourPickerWindow'",
-          "rect": [208, 198, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [208, 198, 13, 11],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [208, 204, 13, 6],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [544, 152, 13, 5],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGPath line",
-          "rect": [362, 353, 13, 5],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
-          "rect": [782, 93, 12, 13],
-          "reason": "full"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
-          "rect": [782, 93, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [782, 93, 12, 13],
-          "reason": "full"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [782, 93, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonbigWindow'",
-          "rect": [571, 144, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [571, 144, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtoncolourPickerWindow'",
-          "rect": [235, 196, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
-          "rect": [235, 196, 12, 13],
-          "reason": "appeared"
-        },
-        {
-          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
           "rect": [615, 83, 12, 12],
-          "reason": "full"
+          "reason": "geometry"
         },
         {
-          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "object": "LayoutSVGPath line",
           "rect": [615, 83, 12, 12],
-          "reason": "full"
+          "reason": "geometry"
         },
         {
-          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
-          "rect": [615, 116, 12, 11],
-          "reason": "full"
+          "object": "LayoutSVGPath line",
+          "rect": [782, 94, 12, 11],
+          "reason": "geometry"
         },
         {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [615, 116, 12, 11],
-          "reason": "full"
+          "object": "LayoutSVGPath line",
+          "rect": [782, 94, 12, 11],
+          "reason": "geometry"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [782, 94, 12, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [782, 94, 12, 11],
+          "reason": "appeared"
         },
         {
           "object": "LayoutSVGRect rect",
@@ -802,13 +582,33 @@
           "reason": "geometry"
         },
         {
-          "object": "LayoutSVGContainer use id='minimizeButtonnestedWindow'",
-          "rect": [349, 185, 12, 11],
+          "object": "LayoutSVGPath line",
+          "rect": [571, 145, 12, 11],
           "reason": "appeared"
         },
         {
-          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
-          "rect": [349, 185, 12, 11],
+          "object": "LayoutSVGPath line",
+          "rect": [571, 145, 12, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [235, 197, 12, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [235, 197, 12, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [615, 123, 12, 3],
+          "reason": "geometry"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [349, 192, 12, 3],
           "reason": "appeared"
         },
         {
@@ -862,11 +662,41 @@
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGPath line",
+          "rect": [454, 379, 11, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [454, 379, 11, 11],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [441, 379, 11, 11],
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGContainer use id='closeButtonnestedWindow'",
+          "rect": [376, 184, 11, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [376, 184, 11, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [376, 184, 11, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [376, 184, 11, 11],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGContainer use id='maximizeButtonnestedWindow'",
           "rect": [363, 184, 11, 11],
           "reason": "appeared"
@@ -887,6 +717,36 @@
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
+          "rect": [756, 95, 11, 10],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
+          "rect": [756, 95, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [756, 95, 11, 10],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [756, 95, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtonbigWindow'",
+          "rect": [545, 146, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [545, 146, 11, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGContainer use id='maximizeButtonsmallWindow'",
           "rect": [441, 380, 11, 10],
           "reason": "appeared"
@@ -907,6 +767,86 @@
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGContainer use id='minimizeButtonstatusWindow'",
+          "rect": [363, 346, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [363, 346, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtoncolourPickerWindow'",
+          "rect": [209, 198, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [209, 198, 11, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [545, 153, 11, 3],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [363, 354, 11, 3],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [756, 103, 11, 2],
+          "reason": "geometry"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [756, 103, 11, 2],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [428, 388, 11, 2],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGPath line",
+          "rect": [209, 206, 11, 2],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
+          "rect": [783, 94, 10, 11],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
+          "rect": [783, 94, 10, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [783, 94, 10, 11],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [783, 94, 10, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtonbigWindow'",
+          "rect": [572, 145, 10, 11],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [572, 145, 10, 11],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGContainer use id='maximizeButtonnavWindow'",
           "rect": [616, 100, 10, 10],
           "reason": "full"
@@ -917,26 +857,76 @@
           "reason": "full"
         },
         {
+          "object": "LayoutSVGContainer use id='closeButtonnavWindow'",
+          "rect": [616, 84, 10, 10],
+          "reason": "full"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [616, 84, 10, 10],
           "reason": "geometry"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [616, 84, 10, 10],
+          "reason": "full"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtonsmallWindow'",
+          "rect": [455, 380, 10, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [455, 380, 10, 10],
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [455, 380, 10, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtonsmallWindow'",
+          "rect": [428, 380, 10, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [428, 380, 10, 10],
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [428, 380, 10, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='minimizeButtonnestedWindow'",
+          "rect": [350, 185, 10, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [350, 185, 10, 10],
+          "reason": "appeared"
+        },
+        {
+          "object": "LayoutSVGContainer use id='closeButtoncolourPickerWindow'",
+          "rect": [236, 198, 10, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [236, 198, 10, 10],
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='closeButton'",
+          "rect": [236, 198, 10, 10],
+          "reason": "appeared"
+        },
+        {
           "object": "LayoutSVGContainer use id='maximizeButtoncolourPickerWindow'",
           "rect": [223, 198, 10, 10],
           "reason": "appeared"
@@ -957,11 +947,21 @@
           "reason": "appeared"
         },
         {
+          "object": "LayoutSVGContainer use id='minimizeButtonnavWindow'",
+          "rect": [616, 116, 10, 9],
+          "reason": "full"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [616, 116, 10, 9],
           "reason": "geometry"
         },
         {
+          "object": "LayoutSVGViewportContainer svg id='minimizeButton'",
+          "rect": [616, 116, 10, 9],
+          "reason": "full"
+        },
+        {
           "object": "LayoutSVGRect rect",
           "rect": [572, 146, 10, 9],
           "reason": "appeared"
diff --git a/third_party/WebKit/Source/DEPS b/third_party/WebKit/Source/DEPS
index 76ff6b1..a5a830a 100644
--- a/third_party/WebKit/Source/DEPS
+++ b/third_party/WebKit/Source/DEPS
@@ -1,6 +1,7 @@
 include_rules = [
     "+base/debug",
     "+base/macros.h",
+    "+base/memory/weak_ptr.h",
     "+base/gtest_prod_util.h",
     "+build",
     "+services/service_manager/public/cpp/connector.h",
diff --git a/third_party/WebKit/Source/bindings/core/v8/V8ScriptRunner.cpp b/third_party/WebKit/Source/bindings/core/v8/V8ScriptRunner.cpp
index bca3ac7..e0d6b7fa 100644
--- a/third_party/WebKit/Source/bindings/core/v8/V8ScriptRunner.cpp
+++ b/third_party/WebKit/Source/bindings/core/v8/V8ScriptRunner.cpp
@@ -39,7 +39,6 @@
 #include "core/frame/LocalFrame.h"
 #include "core/inspector/InspectorTraceEvents.h"
 #include "core/inspector/ThreadDebugger.h"
-#include "core/loader/resource/ScriptResource.h"
 #include "core/probe/CoreProbes.h"
 #include "platform/Histogram.h"
 #include "platform/bindings/ScriptForbiddenScope.h"
@@ -403,9 +402,8 @@
   return CompileScript(script_state, V8String(isolate, source.Source()),
                        source.Url(), source.SourceMapUrl(),
                        source.StartPosition(), source.SourceLocationType(),
-                       source.GetResource(), source.Streamer(),
-                       cache_metadata_handler, access_control_status,
-                       v8_cache_options, referrer_info);
+                       source.Streamer(), cache_metadata_handler,
+                       access_control_status, v8_cache_options, referrer_info);
 }
 
 v8::MaybeLocal<v8::Script> V8ScriptRunner::CompileScript(
@@ -415,7 +413,6 @@
     const String& source_map_url,
     const TextPosition& script_start_position,
     ScriptSourceLocationType source_location_type,
-    ScriptResource* resource,
     ScriptStreamer* streamer,
     CachedMetadataHandler* cache_handler,
     AccessControlStatus access_control_status,
@@ -428,11 +425,6 @@
                          script_start_position.line_.ZeroBasedInt(),
                          script_start_position.column_.ZeroBasedInt());
 
-  DCHECK(!streamer || resource);
-  DCHECK(!resource || resource->CacheHandler() == cache_handler);
-  DCHECK(!resource ||
-         source_location_type == ScriptSourceLocationType::kExternalFile);
-
   // NOTE: For compatibility with WebCore, ScriptSourceCode's line starts at
   // 1, whereas v8 starts at 0.
   v8::Isolate* isolate = script_state->GetIsolate();
@@ -558,7 +550,7 @@
   // - parser_state: always "not parser inserted" for internal scripts.
   if (!V8ScriptRunner::CompileScript(
            script_state, source, file_name, String(), script_start_position,
-           ScriptSourceLocationType::kInternal, nullptr, nullptr, nullptr,
+           ScriptSourceLocationType::kInternal, nullptr, nullptr,
            kSharableCrossOrigin, kV8CacheOptionsDefault, ReferrerScriptInfo())
            .ToLocal(&script))
     return v8::MaybeLocal<v8::Value>();
diff --git a/third_party/WebKit/Source/bindings/core/v8/V8ScriptRunner.h b/third_party/WebKit/Source/bindings/core/v8/V8ScriptRunner.h
index 0abb6f3..99755a2 100644
--- a/third_party/WebKit/Source/bindings/core/v8/V8ScriptRunner.h
+++ b/third_party/WebKit/Source/bindings/core/v8/V8ScriptRunner.h
@@ -50,7 +50,6 @@
 class CachedMetadata;
 class CachedMetadataHandler;
 class ExecutionContext;
-class ScriptResource;
 class ScriptSourceCode;
 class ScriptStreamer;
 
@@ -65,23 +64,19 @@
 
   // For the following methods, the caller sites have to hold
   // a HandleScope and a ContextScope.
+  // CachedMetadataHandler is set when metadata caching is supported.
   static v8::MaybeLocal<v8::Script> CompileScript(ScriptState*,
                                                   const ScriptSourceCode&,
                                                   CachedMetadataHandler*,
                                                   AccessControlStatus,
                                                   V8CacheOptions,
                                                   const ReferrerScriptInfo&);
-  // CachedMetadataHandler is set when metadata caching is supported. For
-  // normal scripe resources, CachedMetadataHandler is from ScriptResource.
-  // For worker script, ScriptResource is null but CachedMetadataHandler may be
-  // set. When ScriptStreamer is set, ScriptResource must be set.
   static v8::MaybeLocal<v8::Script> CompileScript(ScriptState*,
                                                   v8::Local<v8::String>,
                                                   const String& file_name,
                                                   const String& source_map_url,
                                                   const TextPosition&,
                                                   ScriptSourceLocationType,
-                                                  ScriptResource*,
                                                   ScriptStreamer*,
                                                   CachedMetadataHandler*,
                                                   AccessControlStatus,
diff --git a/third_party/WebKit/Source/bindings/core/v8/V8ScriptRunnerTest.cpp b/third_party/WebKit/Source/bindings/core/v8/V8ScriptRunnerTest.cpp
index befa8b5..9c87c8f 100644
--- a/third_party/WebKit/Source/bindings/core/v8/V8ScriptRunnerTest.cpp
+++ b/third_party/WebKit/Source/bindings/core/v8/V8ScriptRunnerTest.cpp
@@ -62,8 +62,8 @@
     return !V8ScriptRunner::CompileScript(
                 script_state, V8String(script_state->GetIsolate(), Code()),
                 Filename(), String(), WTF::TextPosition(),
-                ScriptSourceLocationType::kExternalFile, resource_.Get(),
-                nullptr, resource_.Get() ? resource_->CacheHandler() : nullptr,
+                ScriptSourceLocationType::kExternalFile, nullptr,
+                resource_.Get() ? resource_->CacheHandler() : nullptr,
                 kNotSharableCrossOrigin, cache_options, ReferrerScriptInfo())
                 .IsEmpty();
   }
diff --git a/third_party/WebKit/Source/core/inspector/BUILD.gn b/third_party/WebKit/Source/core/inspector/BUILD.gn
index fe0f95c..60e2eca 100644
--- a/third_party/WebKit/Source/core/inspector/BUILD.gn
+++ b/third_party/WebKit/Source/core/inspector/BUILD.gn
@@ -129,7 +129,7 @@
   config_values = [ "imported.path=$_imported" ]
 
   inputs = [
-    "browser_protocol.json",
+    "$blink_core_output_dir/inspector/browser_protocol.json",
     v8_inspector_js_protocol,
     "inspector_protocol_config.json",
   ]
@@ -194,6 +194,7 @@
   ]
 
   deps = [
+    ":protocol_convert_to_json",
     ":protocol_version",
   ]
 }
@@ -224,11 +225,30 @@
   ]
 }
 
+action("protocol_convert_to_json") {
+  script = "ConvertProtocolToJSON.py"
+  inputs = [
+    "ConvertProtocolToJSON.py",
+    "browser_protocol.pdl",
+  ]
+  output_file = "$blink_core_output_dir/inspector/browser_protocol.json"
+  outputs = [
+    output_file,
+  ]
+  args = [
+    rebase_path("browser_protocol.pdl", root_build_dir),
+    rebase_path(output_file, root_build_dir),
+  ]
+}
+
 action("protocol_compatibility_check") {
   script = _inspector_protocol_dir + "/CheckProtocolCompatibility.py"
+  deps = [
+    ":protocol_convert_to_json",
+  ]
 
   inputs = [
-    "browser_protocol.json",
+    "$blink_core_output_dir/inspector/browser_protocol.json",
     "browser_protocol-1.3.json",
     v8_inspector_js_protocol,
   ]
@@ -240,7 +260,8 @@
   args = [
     "--stamp",
     rebase_path(_stamp, root_build_dir),
-    rebase_path("browser_protocol.json", root_build_dir),
+    rebase_path("$blink_core_output_dir/inspector/browser_protocol.json",
+                root_build_dir),
     rebase_path(v8_inspector_js_protocol, root_build_dir),
   ]
 }
@@ -248,11 +269,12 @@
 action("protocol_version") {
   deps = [
     ":protocol_compatibility_check",
+    ":protocol_convert_to_json",
   ]
   script = _inspector_protocol_dir + "/ConcatenateProtocols.py"
 
   inputs = [
-    "browser_protocol.json",
+    "$blink_core_output_dir/inspector/browser_protocol.json",
     v8_inspector_js_protocol,
   ]
   output_file = "$blink_core_output_dir/inspector/protocol.json"
@@ -261,7 +283,8 @@
   ]
 
   args = [
-    rebase_path("browser_protocol.json", root_build_dir),
+    rebase_path("$blink_core_output_dir/inspector/browser_protocol.json",
+                root_build_dir),
     rebase_path(v8_inspector_js_protocol, root_build_dir),
     rebase_path(output_file, root_build_dir),
   ]
diff --git a/third_party/WebKit/Source/core/inspector/ConvertProtocolToJSON.py b/third_party/WebKit/Source/core/inspector/ConvertProtocolToJSON.py
index d5afe33..0b2fcfc 100644
--- a/third_party/WebKit/Source/core/inspector/ConvertProtocolToJSON.py
+++ b/third_party/WebKit/Source/core/inspector/ConvertProtocolToJSON.py
@@ -161,18 +161,20 @@
 
 def main(argv):
     if len(argv) < 2:
-        sys.stderr.write("Usage: %s stamp <protocol.pdl>\n" % sys.argv[0])
+        sys.stderr.write("Usage: %s <protocol.pdl> <protocol.json>\n" % sys.argv[0])
         return 1
     global file_name
-    file_name = os.path.normpath(argv[1])
+    file_name = os.path.normpath(argv[0])
     input_file = open(file_name, "r")
     pdl_string = input_file.read()
     protocol = parse(pdl_string)
-    output_file = open(argv[1].replace('.pdl', '.json'), "w")
+    output_file = open(argv[0].replace('.pdl', '.json'), "w")
     json.dump(protocol, output_file, indent=4, separators=(',', ': '))
     output_file.close()
-    with open(os.path.normpath(argv[0]), 'a') as _:
-        pass
+
+    output_file = open(os.path.normpath(argv[1]), "w")
+    json.dump(protocol, output_file, indent=4, separators=(',', ': '))
+    output_file.close()
 
 
 if __name__ == '__main__':
diff --git a/third_party/WebKit/Source/core/loader/DocumentThreadableLoader.cpp b/third_party/WebKit/Source/core/loader/DocumentThreadableLoader.cpp
index 862c501..2fe43cf7 100644
--- a/third_party/WebKit/Source/core/loader/DocumentThreadableLoader.cpp
+++ b/third_party/WebKit/Source/core/loader/DocumentThreadableLoader.cpp
@@ -32,6 +32,7 @@
 #include "core/loader/DocumentThreadableLoader.h"
 
 #include <memory>
+#include "base/memory/weak_ptr.h"
 #include "core/dom/Document.h"
 #include "core/frame/FrameConsole.h"
 #include "core/frame/LocalFrame.h"
@@ -59,7 +60,6 @@
 #include "platform/weborigin/SecurityPolicy.h"
 #include "platform/wtf/Assertions.h"
 #include "platform/wtf/PtrUtil.h"
-#include "platform/wtf/WeakPtr.h"
 #include "public/platform/Platform.h"
 #include "public/platform/TaskType.h"
 #include "public/platform/WebCORS.h"
@@ -81,7 +81,7 @@
         : factory_(this) {
       Platform::Current()->CurrentThread()->GetWebTaskRunner()->PostTask(
           BLINK_FROM_HERE,
-          WTF::Bind(&EmptyDataReader::Notify, factory_.CreateWeakPtr(),
+          WTF::Bind(&EmptyDataReader::Notify, factory_.GetWeakPtr(),
                     WTF::Unretained(client)));
     }
 
@@ -99,7 +99,7 @@
     void Notify(WebDataConsumerHandle::Client* client) {
       client->DidGetReadable();
     }
-    WeakPtrFactory<EmptyDataReader> factory_;
+    base::WeakPtrFactory<EmptyDataReader> factory_;
   };
 
   std::unique_ptr<Reader> ObtainReader(Client* client) override {
diff --git a/third_party/WebKit/Source/core/loader/ImageLoader.cpp b/third_party/WebKit/Source/core/loader/ImageLoader.cpp
index 02111450..d46dfdd 100644
--- a/third_party/WebKit/Source/core/loader/ImageLoader.cpp
+++ b/third_party/WebKit/Source/core/loader/ImageLoader.cpp
@@ -131,14 +131,14 @@
     script_state_ = nullptr;
   }
 
-  WeakPtr<Task> CreateWeakPtr() { return weak_factory_.CreateWeakPtr(); }
+  base::WeakPtr<Task> GetWeakPtr() { return weak_factory_.GetWeakPtr(); }
 
  private:
   WeakPersistent<ImageLoader> loader_;
   BypassMainWorldBehavior should_bypass_main_world_csp_;
   UpdateFromElementBehavior update_behavior_;
   scoped_refptr<ScriptState> script_state_;
-  WeakPtrFactory<Task> weak_factory_;
+  base::WeakPtrFactory<Task> weak_factory_;
   ReferrerPolicy referrer_policy_;
   KURL request_url_;
 };
@@ -342,7 +342,7 @@
     ReferrerPolicy referrer_policy) {
   std::unique_ptr<Task> task =
       Task::Create(this, update_behavior, referrer_policy);
-  pending_task_ = task->CreateWeakPtr();
+  pending_task_ = task->GetWeakPtr();
   Microtask::EnqueueMicrotask(
       WTF::Bind(&Task::Run, WTF::Passed(std::move(task))));
   delay_until_do_update_from_element_ =
diff --git a/third_party/WebKit/Source/core/loader/ImageLoader.h b/third_party/WebKit/Source/core/loader/ImageLoader.h
index 6c26172..3e53ed1 100644
--- a/third_party/WebKit/Source/core/loader/ImageLoader.h
+++ b/third_party/WebKit/Source/core/loader/ImageLoader.h
@@ -24,6 +24,7 @@
 #define ImageLoader_h
 
 #include <memory>
+#include "base/memory/weak_ptr.h"
 #include "bindings/core/v8/ScriptPromise.h"
 #include "bindings/core/v8/ScriptPromiseResolver.h"
 #include "core/CoreExport.h"
@@ -32,7 +33,6 @@
 #include "core/loader/resource/ImageResourceObserver.h"
 #include "platform/heap/Handle.h"
 #include "platform/wtf/HashSet.h"
-#include "platform/wtf/WeakPtr.h"
 #include "platform/wtf/text/AtomicString.h"
 #include "public/platform/TaskType.h"
 
@@ -184,7 +184,7 @@
   Member<ImageResource> image_resource_for_image_document_;
 
   AtomicString failed_load_url_;
-  WeakPtr<Task> pending_task_;  // owned by Microtask
+  base::WeakPtr<Task> pending_task_;  // owned by Microtask
   std::unique_ptr<IncrementLoadEventDelayCount>
       delay_until_do_update_from_element_;
 
diff --git a/third_party/WebKit/Source/core/offscreencanvas/OffscreenCanvas.cpp b/third_party/WebKit/Source/core/offscreencanvas/OffscreenCanvas.cpp
index 9cc47b0..ae8e0aac7 100644
--- a/third_party/WebKit/Source/core/offscreencanvas/OffscreenCanvas.cpp
+++ b/third_party/WebKit/Source/core/offscreencanvas/OffscreenCanvas.cpp
@@ -247,8 +247,8 @@
 ImageBuffer* OffscreenCanvas::GetOrCreateImageBuffer() {
   if (!image_buffer_) {
     bool is_accelerated_2d_canvas_blacklisted = true;
-    WeakPtr<WebGraphicsContext3DProviderWrapper> context_provider_wrapper =
-        SharedGpuContext::ContextProviderWrapper();
+    base::WeakPtr<WebGraphicsContext3DProviderWrapper>
+        context_provider_wrapper = SharedGpuContext::ContextProviderWrapper();
     if (context_provider_wrapper) {
       const gpu::GpuFeatureInfo& gpu_feature_info =
           context_provider_wrapper->ContextProvider()->GetGpuFeatureInfo();
diff --git a/third_party/WebKit/Source/core/workers/DedicatedWorkerObjectProxy.h b/third_party/WebKit/Source/core/workers/DedicatedWorkerObjectProxy.h
index 136fa0b..f73f652 100644
--- a/third_party/WebKit/Source/core/workers/DedicatedWorkerObjectProxy.h
+++ b/third_party/WebKit/Source/core/workers/DedicatedWorkerObjectProxy.h
@@ -38,7 +38,6 @@
 #include "core/workers/ThreadedObjectProxyBase.h"
 #include "core/workers/WorkerReportingProxy.h"
 #include "platform/heap/Handle.h"
-#include "platform/wtf/WeakPtr.h"
 
 namespace v8_inspector {
 struct V8StackTraceId;
diff --git a/third_party/WebKit/Source/modules/accessibility/AXRelationCache.h b/third_party/WebKit/Source/modules/accessibility/AXRelationCache.h
index 5a131d4..599692c7 100644
--- a/third_party/WebKit/Source/modules/accessibility/AXRelationCache.h
+++ b/third_party/WebKit/Source/modules/accessibility/AXRelationCache.h
@@ -33,7 +33,6 @@
 #include "modules/accessibility/AXObjectCacheImpl.h"
 #include "platform/wtf/HashMap.h"
 #include "platform/wtf/HashSet.h"
-#include "platform/wtf/WeakPtr.h"
 
 namespace blink {
 
diff --git a/third_party/WebKit/Source/modules/mediacapturefromelement/OnRequestCanvasDrawListener.cpp b/third_party/WebKit/Source/modules/mediacapturefromelement/OnRequestCanvasDrawListener.cpp
index 059423b..e490a47 100644
--- a/third_party/WebKit/Source/modules/mediacapturefromelement/OnRequestCanvasDrawListener.cpp
+++ b/third_party/WebKit/Source/modules/mediacapturefromelement/OnRequestCanvasDrawListener.cpp
@@ -22,7 +22,7 @@
 
 void OnRequestCanvasDrawListener::SendNewFrame(
     sk_sp<SkImage> image,
-    WeakPtr<WebGraphicsContext3DProviderWrapper> context_provider) {
+    base::WeakPtr<WebGraphicsContext3DProviderWrapper> context_provider) {
   frame_capture_requested_ = false;
   CanvasDrawListener::SendNewFrame(image, context_provider);
 }
diff --git a/third_party/WebKit/Source/modules/mediacapturefromelement/OnRequestCanvasDrawListener.h b/third_party/WebKit/Source/modules/mediacapturefromelement/OnRequestCanvasDrawListener.h
index 1fda609..dec7668 100644
--- a/third_party/WebKit/Source/modules/mediacapturefromelement/OnRequestCanvasDrawListener.h
+++ b/third_party/WebKit/Source/modules/mediacapturefromelement/OnRequestCanvasDrawListener.h
@@ -6,9 +6,9 @@
 #define OnRequestCanvasDrawListener_h
 
 #include <memory>
+#include "base/memory/weak_ptr.h"
 #include "core/html/canvas/CanvasDrawListener.h"
 #include "platform/heap/Handle.h"
-#include "platform/wtf/WeakPtr.h"
 #include "public/platform/WebCanvasCaptureHandler.h"
 #include "third_party/skia/include/core/SkRefCnt.h"
 
@@ -23,8 +23,9 @@
   ~OnRequestCanvasDrawListener() override;
   static OnRequestCanvasDrawListener* Create(
       std::unique_ptr<WebCanvasCaptureHandler>);
-  void SendNewFrame(sk_sp<SkImage>,
-                    WeakPtr<WebGraphicsContext3DProviderWrapper>) override;
+  void SendNewFrame(
+      sk_sp<SkImage>,
+      base::WeakPtr<WebGraphicsContext3DProviderWrapper>) override;
 
   void Trace(blink::Visitor* visitor) override {}
 
diff --git a/third_party/WebKit/Source/modules/mediacapturefromelement/TimedCanvasDrawListener.cpp b/third_party/WebKit/Source/modules/mediacapturefromelement/TimedCanvasDrawListener.cpp
index ea50e4d1..cc41ac9a 100644
--- a/third_party/WebKit/Source/modules/mediacapturefromelement/TimedCanvasDrawListener.cpp
+++ b/third_party/WebKit/Source/modules/mediacapturefromelement/TimedCanvasDrawListener.cpp
@@ -37,7 +37,7 @@
 
 void TimedCanvasDrawListener::SendNewFrame(
     sk_sp<SkImage> image,
-    WeakPtr<WebGraphicsContext3DProviderWrapper> context_provider) {
+    base::WeakPtr<WebGraphicsContext3DProviderWrapper> context_provider) {
   frame_capture_requested_ = false;
   CanvasDrawListener::SendNewFrame(image, context_provider);
 }
diff --git a/third_party/WebKit/Source/modules/mediacapturefromelement/TimedCanvasDrawListener.h b/third_party/WebKit/Source/modules/mediacapturefromelement/TimedCanvasDrawListener.h
index c1a43be11..0795ac1 100644
--- a/third_party/WebKit/Source/modules/mediacapturefromelement/TimedCanvasDrawListener.h
+++ b/third_party/WebKit/Source/modules/mediacapturefromelement/TimedCanvasDrawListener.h
@@ -6,10 +6,10 @@
 #define TimedCanvasDrawListener_h
 
 #include <memory>
+#include "base/memory/weak_ptr.h"
 #include "core/html/canvas/CanvasDrawListener.h"
 #include "platform/Timer.h"
 #include "platform/heap/Handle.h"
-#include "platform/wtf/WeakPtr.h"
 
 #include "public/platform/WebCanvasCaptureHandler.h"
 #include "third_party/skia/include/core/SkRefCnt.h"
@@ -28,8 +28,9 @@
       std::unique_ptr<WebCanvasCaptureHandler>,
       double frame_rate,
       ExecutionContext*);
-  void SendNewFrame(sk_sp<SkImage>,
-                    WeakPtr<WebGraphicsContext3DProviderWrapper>) override;
+  void SendNewFrame(
+      sk_sp<SkImage>,
+      base::WeakPtr<WebGraphicsContext3DProviderWrapper>) override;
 
   void Trace(blink::Visitor* visitor) override {}
 
diff --git a/third_party/WebKit/Source/modules/serviceworkers/NavigationPreloadManager.idl b/third_party/WebKit/Source/modules/serviceworkers/NavigationPreloadManager.idl
index 7c830b6..6176378 100644
--- a/third_party/WebKit/Source/modules/serviceworkers/NavigationPreloadManager.idl
+++ b/third_party/WebKit/Source/modules/serviceworkers/NavigationPreloadManager.idl
@@ -2,17 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// TODO(falken): Revise link when this lands in the spec:
-// https://github.com/w3c/ServiceWorker/issues/920
+// https://w3c.github.io/ServiceWorker/#navigation-preload-manager
 [
+    SecureContext,
     Exposed=(Window,Worker)
 ] interface NavigationPreloadManager {
-    // TODO(mgiuca): Put SecureContext on the interface, not individual methods.
-    // Currently prevented due to clash with OriginTrialEnabled. This can be
-    // resolved either when OriginTrialEnabled is removed, or
-    // https://crbug.com/695123 is fixed.
-    [SecureContext, CallWith=ScriptState] Promise<void> enable();
-    [SecureContext, CallWith=ScriptState] Promise<void> disable();
-    [SecureContext, CallWith=ScriptState] Promise<void> setHeaderValue(ByteString value);
-    [SecureContext, CallWith=ScriptState] Promise<NavigationPreloadState> getState();
+    [CallWith=ScriptState] Promise<void> enable();
+    [CallWith=ScriptState] Promise<void> disable();
+    [CallWith=ScriptState] Promise<void> setHeaderValue(ByteString value);
+    [CallWith=ScriptState] Promise<NavigationPreloadState> getState();
 };
diff --git a/third_party/WebKit/Source/modules/serviceworkers/NavigationPreloadState.idl b/third_party/WebKit/Source/modules/serviceworkers/NavigationPreloadState.idl
index 2a06381..1c8c4a1 100644
--- a/third_party/WebKit/Source/modules/serviceworkers/NavigationPreloadState.idl
+++ b/third_party/WebKit/Source/modules/serviceworkers/NavigationPreloadState.idl
@@ -2,8 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// TODO(falken): Revise link when this lands in the spec:
-// https://github.com/w3c/ServiceWorker/issues/920
+// https://w3c.github.io/ServiceWorker/#navigation-preload-manager
 dictionary NavigationPreloadState {
     boolean enabled = false;
     ByteString headerValue;
diff --git a/third_party/WebKit/Source/modules/webgl/WebGLRenderingContextBase.cpp b/third_party/WebKit/Source/modules/webgl/WebGLRenderingContextBase.cpp
index 1802d96..12b7d0ad 100644
--- a/third_party/WebKit/Source/modules/webgl/WebGLRenderingContextBase.cpp
+++ b/third_party/WebKit/Source/modules/webgl/WebGLRenderingContextBase.cpp
@@ -793,7 +793,7 @@
 scoped_refptr<StaticBitmapImage> WebGLRenderingContextBase::MakeImageSnapshot(
     SkImageInfo& image_info) {
   GetDrawingBuffer()->ResolveAndBindForReadAndDraw();
-  WeakPtr<WebGraphicsContext3DProviderWrapper> shared_context_wrapper =
+  base::WeakPtr<WebGraphicsContext3DProviderWrapper> shared_context_wrapper =
       SharedGpuContext::ContextProviderWrapper();
   if (!shared_context_wrapper)
     return nullptr;
diff --git a/third_party/WebKit/Source/platform/CrossThreadCopier.h b/third_party/WebKit/Source/platform/CrossThreadCopier.h
index c7fff9fc..8cc982f3 100644
--- a/third_party/WebKit/Source/platform/CrossThreadCopier.h
+++ b/third_party/WebKit/Source/platform/CrossThreadCopier.h
@@ -33,6 +33,7 @@
 
 #include <memory>
 #include "base/memory/scoped_refptr.h"
+#include "base/memory/weak_ptr.h"
 #include "mojo/public/cpp/bindings/interface_ptr_info.h"
 #include "mojo/public/cpp/bindings/interface_request.h"
 #include "platform/PlatformExport.h"
@@ -40,7 +41,6 @@
 #include "platform/wtf/Forward.h"
 #include "platform/wtf/Functional.h"  // FunctionThreadAffinity
 #include "platform/wtf/TypeTraits.h"
-#include "platform/wtf/WeakPtr.h"
 #include "third_party/WebKit/common/message_port/message_port_channel.h"
 
 namespace base {
@@ -192,8 +192,8 @@
 };
 
 template <typename T>
-struct CrossThreadCopier<WeakPtr<T>>
-    : public CrossThreadCopierPassThrough<WeakPtr<T>> {
+struct CrossThreadCopier<base::WeakPtr<T>>
+    : public CrossThreadCopierPassThrough<base::WeakPtr<T>> {
   STATIC_ONLY(CrossThreadCopier);
 };
 
diff --git a/third_party/WebKit/Source/platform/Timer.cpp b/third_party/WebKit/Source/platform/Timer.cpp
index f922674..1a74be8 100644
--- a/third_party/WebKit/Source/platform/Timer.cpp
+++ b/third_party/WebKit/Source/platform/Timer.cpp
@@ -72,7 +72,7 @@
 
   repeat_interval_ = TimeDelta();
   next_fire_time_ = TimeTicks();
-  weak_ptr_factory_.RevokeAll();
+  weak_ptr_factory_.InvalidateWeakPtrs();
 }
 
 TimeDelta TimerBase::NextFireIntervalDelta() const {
@@ -94,7 +94,7 @@
   }
 
   bool active = IsActive();
-  weak_ptr_factory_.RevokeAll();
+  weak_ptr_factory_.InvalidateWeakPtrs();
   web_task_runner_ = std::move(task_runner);
 
   if (!active)
@@ -127,11 +127,11 @@
     next_fire_time_ = new_time;
 
     // Cancel any previously posted task.
-    weak_ptr_factory_.RevokeAll();
+    weak_ptr_factory_.InvalidateWeakPtrs();
 
     TimerTaskRunner()->PostDelayedTask(
         location_,
-        WTF::Bind(&TimerBase::RunInternal, weak_ptr_factory_.CreateWeakPtr()),
+        WTF::Bind(&TimerBase::RunInternal, weak_ptr_factory_.GetWeakPtr()),
         delay);
   }
 }
@@ -141,7 +141,7 @@
   if (!CanFire())
     return;
 
-  weak_ptr_factory_.RevokeAll();
+  weak_ptr_factory_.InvalidateWeakPtrs();
 
   TRACE_EVENT0("blink", "TimerBase::run");
 #if DCHECK_IS_ON()
diff --git a/third_party/WebKit/Source/platform/Timer.h b/third_party/WebKit/Source/platform/Timer.h
index 2dfb7c8..85204772 100644
--- a/third_party/WebKit/Source/platform/Timer.h
+++ b/third_party/WebKit/Source/platform/Timer.h
@@ -26,6 +26,7 @@
 #ifndef Timer_h
 #define Timer_h
 
+#include "base/memory/weak_ptr.h"
 #include "base/time/time.h"
 #include "platform/PlatformExport.h"
 #include "platform/WebTaskRunner.h"
@@ -35,7 +36,6 @@
 #include "platform/wtf/Noncopyable.h"
 #include "platform/wtf/Threading.h"
 #include "platform/wtf/Time.h"
-#include "platform/wtf/WeakPtr.h"
 #include "public/platform/WebTraceLocation.h"
 
 namespace blink {
@@ -125,7 +125,7 @@
 #if DCHECK_IS_ON()
   ThreadIdentifier thread_;
 #endif
-  WTF::WeakPtrFactory<TimerBase> weak_ptr_factory_;
+  base::WeakPtrFactory<TimerBase> weak_ptr_factory_;
 
   friend class ThreadTimers;
   friend class TimerHeapLessThanFunction;
diff --git a/third_party/WebKit/Source/platform/WebTaskRunner.cpp b/third_party/WebKit/Source/platform/WebTaskRunner.cpp
index 0eb1065..5e2b383 100644
--- a/third_party/WebKit/Source/platform/WebTaskRunner.cpp
+++ b/third_party/WebKit/Source/platform/WebTaskRunner.cpp
@@ -15,11 +15,11 @@
 template <>
 struct CallbackCancellationTraits<
     RunnerMethodType,
-    std::tuple<WTF::WeakPtr<blink::TaskHandle::Runner>, blink::TaskHandle>> {
+    std::tuple<base::WeakPtr<blink::TaskHandle::Runner>, blink::TaskHandle>> {
   static constexpr bool is_cancellable = true;
 
   static bool IsCancelled(RunnerMethodType,
-                          const WTF::WeakPtr<blink::TaskHandle::Runner>&,
+                          const base::WeakPtr<blink::TaskHandle::Runner>&,
                           const blink::TaskHandle& handle) {
     return !handle.IsActive();
   }
@@ -42,13 +42,13 @@
   explicit Runner(WTF::Closure task)
       : task_(std::move(task)), weak_ptr_factory_(this) {}
 
-  WTF::WeakPtr<Runner> AsWeakPtr() { return weak_ptr_factory_.CreateWeakPtr(); }
+  base::WeakPtr<Runner> AsWeakPtr() { return weak_ptr_factory_.GetWeakPtr(); }
 
   bool IsActive() const { return task_ && !task_.IsCancelled(); }
 
   void Cancel() {
     WTF::Closure task = std::move(task_);
-    weak_ptr_factory_.RevokeAll();
+    weak_ptr_factory_.InvalidateWeakPtrs();
   }
 
   ~Runner() { Cancel(); }
@@ -71,13 +71,13 @@
   // |m_task| when the wrapped WTF::Closure is deleted.
   void Run(const TaskHandle&) {
     WTF::Closure task = std::move(task_);
-    weak_ptr_factory_.RevokeAll();
+    weak_ptr_factory_.InvalidateWeakPtrs();
     std::move(task).Run();
   }
 
  private:
   WTF::Closure task_;
-  WTF::WeakPtrFactory<Runner> weak_ptr_factory_;
+  base::WeakPtrFactory<Runner> weak_ptr_factory_;
 
   DISALLOW_COPY_AND_ASSIGN(Runner);
 };
diff --git a/third_party/WebKit/Source/platform/WebTaskRunner.h b/third_party/WebKit/Source/platform/WebTaskRunner.h
index e2c2d59..08886f88 100644
--- a/third_party/WebKit/Source/platform/WebTaskRunner.h
+++ b/third_party/WebKit/Source/platform/WebTaskRunner.h
@@ -12,7 +12,6 @@
 #include "platform/wtf/Functional.h"
 #include "platform/wtf/RefCounted.h"
 #include "platform/wtf/Time.h"
-#include "platform/wtf/WeakPtr.h"
 #include "public/platform/WebCommon.h"
 #include "public/platform/WebTraceLocation.h"
 
diff --git a/third_party/WebKit/Source/platform/WebTaskRunnerTest.cpp b/third_party/WebKit/Source/platform/WebTaskRunnerTest.cpp
index ba09c95..3591cc3 100644
--- a/third_party/WebKit/Source/platform/WebTaskRunnerTest.cpp
+++ b/third_party/WebKit/Source/platform/WebTaskRunnerTest.cpp
@@ -22,17 +22,17 @@
  public:
   CancellationTestHelper() : weak_ptr_factory_(this) {}
 
-  WeakPtr<CancellationTestHelper> CreateWeakPtr() {
-    return weak_ptr_factory_.CreateWeakPtr();
+  base::WeakPtr<CancellationTestHelper> GetWeakPtr() {
+    return weak_ptr_factory_.GetWeakPtr();
   }
 
-  void RevokeWeakPtrs() { weak_ptr_factory_.RevokeAll(); }
+  void RevokeWeakPtrs() { weak_ptr_factory_.InvalidateWeakPtrs(); }
   void IncrementCounter() { ++counter_; }
   int Counter() const { return counter_; }
 
  private:
   int counter_ = 0;
-  WeakPtrFactory<CancellationTestHelper> weak_ptr_factory_;
+  base::WeakPtrFactory<CancellationTestHelper> weak_ptr_factory_;
 };
 
 }  // namespace
@@ -140,7 +140,7 @@
   CancellationTestHelper helper;
   handle = task_runner->PostCancellableTask(
       BLINK_FROM_HERE, WTF::Bind(&CancellationTestHelper::IncrementCounter,
-                                 helper.CreateWeakPtr()));
+                                 helper.GetWeakPtr()));
   EXPECT_EQ(0, helper.Counter());
 
   // The cancellation of the posted task should be propagated to TaskHandle.
diff --git a/third_party/WebKit/Source/platform/audio/VectorMath.cpp b/third_party/WebKit/Source/platform/audio/VectorMath.cpp
index 2f1c36f9..0a7cf24 100644
--- a/third_party/WebKit/Source/platform/audio/VectorMath.cpp
+++ b/third_party/WebKit/Source/platform/audio/VectorMath.cpp
@@ -683,8 +683,38 @@
   float low_threshold = *low_threshold_p;
   float high_threshold = *high_threshold_p;
 
-// FIXME: Optimize for SSE2.
-#if WTF_CPU_ARM_NEON
+#if DCHECK_IS_ON()
+  // Do the same DCHECKs that |clampTo| would do so that optimization paths do
+  // not have to do them.
+  for (size_t i = 0u; i < frames_to_process; ++i)
+    DCHECK(!std::isnan(source_p[i]));
+  // This also ensures that thresholds are not NaNs.
+  DCHECK_LE(low_threshold, high_threshold);
+#endif
+
+#if defined(ARCH_CPU_X86_FAMILY)
+  if (source_stride == 1 && dest_stride == 1) {
+    size_t i = 0u;
+
+    // If the source_p address is not 16-byte aligned, the first several
+    // frames  (at most three) should be processed separately.
+    for (; !SSE::IsAligned(source_p + i) && i < frames_to_process; ++i)
+      dest_p[i] = clampTo(source_p[i], low_threshold, high_threshold);
+
+    // Now the source_p+i address is 16-byte aligned. Start to apply SSE.
+    size_t sse_frames_to_process =
+        (frames_to_process - i) & SSE::kFramesToProcessMask;
+    if (sse_frames_to_process > 0u) {
+      SSE::Vclip(source_p + i, &low_threshold, &high_threshold, dest_p + i,
+                 sse_frames_to_process);
+      i += sse_frames_to_process;
+    }
+
+    source_p += i;
+    dest_p += i;
+    n -= i;
+  }
+#elif WTF_CPU_ARM_NEON
   if ((source_stride == 1) && (dest_stride == 1)) {
     int tail_frames = n % 4;
     const float* end_p = dest_p + n - tail_frames;
diff --git a/third_party/WebKit/Source/platform/audio/cpu/x86/VectorMathImpl.cpp b/third_party/WebKit/Source/platform/audio/cpu/x86/VectorMathImpl.cpp
index 4953f6b8..c802ab4e 100644
--- a/third_party/WebKit/Source/platform/audio/cpu/x86/VectorMathImpl.cpp
+++ b/third_party/WebKit/Source/platform/audio/cpu/x86/VectorMathImpl.cpp
@@ -9,6 +9,7 @@
 #include "platform/wtf/Assertions.h"
 
 #include <algorithm>
+#include <cmath>
 
 #include <xmmintrin.h>
 
@@ -72,6 +73,40 @@
 #undef ADD_ALL
 }
 
+// dest[k] = clip(source[k], low_threshold, high_threshold)
+//         = max(low_threshold, min(high_threshold, source[k]))
+void Vclip(const float* source_p,
+           const float* low_threshold_p,
+           const float* high_threshold_p,
+           float* dest_p,
+           size_t frames_to_process) {
+  const float* const source_end_p = source_p + frames_to_process;
+
+  DCHECK(IsAligned(source_p));
+  DCHECK_EQ(0u, frames_to_process % kPackedFloatsPerRegister);
+
+  MType m_low_threshold = MM_PS(set1)(*low_threshold_p);
+  MType m_high_threshold = MM_PS(set1)(*high_threshold_p);
+
+#define CLIP_ALL(storeDest)                                                  \
+  while (source_p < source_end_p) {                                          \
+    MType m_source = MM_PS(load)(source_p);                                  \
+    MType m_dest =                                                           \
+        MM_PS(max)(m_low_threshold, MM_PS(min)(m_high_threshold, m_source)); \
+    MM_PS(storeDest)(dest_p, m_dest);                                        \
+    source_p += kPackedFloatsPerRegister;                                    \
+    dest_p += kPackedFloatsPerRegister;                                      \
+  }
+
+  if (IsAligned(dest_p)) {
+    CLIP_ALL(store);
+  } else {
+    CLIP_ALL(storeu);
+  }
+
+#undef CLIP_ALL
+}
+
 // max = max(abs(source[k])) for all k
 void Vmaxmgv(const float* source_p, float* max_p, size_t frames_to_process) {
   constexpr uint32_t kMask = 0x7FFFFFFFu;
diff --git a/third_party/WebKit/Source/platform/fonts/FontFallbackList.h b/third_party/WebKit/Source/platform/fonts/FontFallbackList.h
index 923f366..49fad83 100644
--- a/third_party/WebKit/Source/platform/fonts/FontFallbackList.h
+++ b/third_party/WebKit/Source/platform/fonts/FontFallbackList.h
@@ -21,6 +21,7 @@
 #ifndef FontFallbackList_h
 #define FontFallbackList_h
 
+#include "base/memory/weak_ptr.h"
 #include "platform/fonts/FallbackListCompositeKey.h"
 #include "platform/fonts/FontCache.h"
 #include "platform/fonts/FontSelector.h"
@@ -29,7 +30,6 @@
 #include "platform/wtf/Allocator.h"
 #include "platform/wtf/Forward.h"
 #include "platform/wtf/RefCounted.h"
-#include "platform/wtf/WeakPtr.h"
 
 namespace blink {
 
@@ -99,7 +99,7 @@
   mutable int family_index_;
   unsigned short generation_;
   mutable bool has_loading_fallback_ : 1;
-  mutable WeakPtr<ShapeCache> shape_cache_;
+  mutable base::WeakPtr<ShapeCache> shape_cache_;
 };
 
 }  // namespace blink
diff --git a/third_party/WebKit/Source/platform/fonts/shaping/ShapeCache.h b/third_party/WebKit/Source/platform/fonts/shaping/ShapeCache.h
index 0e1ee81..e1d8f9b 100644
--- a/third_party/WebKit/Source/platform/fonts/shaping/ShapeCache.h
+++ b/third_party/WebKit/Source/platform/fonts/shaping/ShapeCache.h
@@ -27,13 +27,13 @@
 #ifndef ShapeCache_h
 #define ShapeCache_h
 
+#include "base/memory/weak_ptr.h"
 #include "platform/fonts/shaping/ShapeResult.h"
 #include "platform/text/TextRun.h"
 #include "platform/wtf/Forward.h"
 #include "platform/wtf/HashFunctions.h"
 #include "platform/wtf/HashSet.h"
 #include "platform/wtf/HashTableDeletedValueType.h"
-#include "platform/wtf/WeakPtr.h"
 
 namespace blink {
 
@@ -149,7 +149,7 @@
     return self_byte_size;
   }
 
-  WeakPtr<ShapeCache> GetWeakPtr() { return weak_factory_.CreateWeakPtr(); }
+  base::WeakPtr<ShapeCache> GetWeakPtr() { return weak_factory_.GetWeakPtr(); }
 
  private:
   ShapeCacheEntry* AddSlowCase(const TextRun& run, ShapeCacheEntry entry) {
@@ -236,7 +236,7 @@
 
   SingleCharMap single_char_map_;
   SmallStringMap short_string_map_;
-  WeakPtrFactory<ShapeCache> weak_factory_;
+  base::WeakPtrFactory<ShapeCache> weak_factory_;
   unsigned version_;
 };
 
diff --git a/third_party/WebKit/Source/platform/loader/fetch/RawResource.h b/third_party/WebKit/Source/platform/loader/fetch/RawResource.h
index 65dfd5d..8d59b043 100644
--- a/third_party/WebKit/Source/platform/loader/fetch/RawResource.h
+++ b/third_party/WebKit/Source/platform/loader/fetch/RawResource.h
@@ -24,12 +24,12 @@
 #define RawResource_h
 
 #include <memory>
+
 #include "platform/PlatformExport.h"
 #include "platform/loader/fetch/BufferingDataPipeWriter.h"
 #include "platform/loader/fetch/Resource.h"
 #include "platform/loader/fetch/ResourceClient.h"
 #include "platform/loader/fetch/ResourceLoaderOptions.h"
-#include "platform/wtf/WeakPtr.h"
 #include "public/platform/WebDataConsumerHandle.h"
 
 namespace blink {
diff --git a/third_party/WebKit/Source/platform/scheduler/base/task_queue.h b/third_party/WebKit/Source/platform/scheduler/base/task_queue.h
index 0088630..9af508e 100644
--- a/third_party/WebKit/Source/platform/scheduler/base/task_queue.h
+++ b/third_party/WebKit/Source/platform/scheduler/base/task_queue.h
@@ -6,6 +6,7 @@
 #define THIRD_PARTY_WEBKIT_SOURCE_PLATFORM_SCHEDULER_BASE_TASK_QUEUE_H_
 
 #include "base/macros.h"
+#include "base/memory/weak_ptr.h"
 #include "base/message_loop/message_loop.h"
 #include "base/optional.h"
 #include "base/single_thread_task_runner.h"
diff --git a/third_party/WebKit/Source/platform/scheduler/base/thread_controller_impl.h b/third_party/WebKit/Source/platform/scheduler/base/thread_controller_impl.h
index 6010f81..31dd466 100644
--- a/third_party/WebKit/Source/platform/scheduler/base/thread_controller_impl.h
+++ b/third_party/WebKit/Source/platform/scheduler/base/thread_controller_impl.h
@@ -10,6 +10,7 @@
 #include "base/cancelable_callback.h"
 #include "base/debug/task_annotator.h"
 #include "base/macros.h"
+#include "base/memory/weak_ptr.h"
 #include "base/run_loop.h"
 #include "base/sequence_checker.h"
 #include "base/single_thread_task_runner.h"
diff --git a/third_party/WebKit/Source/platform/scheduler/child/idle_canceled_delayed_task_sweeper.h b/third_party/WebKit/Source/platform/scheduler/child/idle_canceled_delayed_task_sweeper.h
index b43620f..88a42fb7 100644
--- a/third_party/WebKit/Source/platform/scheduler/child/idle_canceled_delayed_task_sweeper.h
+++ b/third_party/WebKit/Source/platform/scheduler/child/idle_canceled_delayed_task_sweeper.h
@@ -6,6 +6,7 @@
 #define THIRD_PARTY_WEBKIT_SOURCE_PLATFORM_SCHEDULER_CHILD_IDLE_CANCELED_DELAYED_TASK_SWEEPER_H_
 
 #include "base/macros.h"
+#include "base/memory/weak_ptr.h"
 #include "platform/scheduler/child/scheduler_helper.h"
 #include "public/platform/scheduler/child/single_thread_idle_task_runner.h"
 
diff --git a/third_party/WebKit/Source/platform/scheduler/child/idle_helper.h b/third_party/WebKit/Source/platform/scheduler/child/idle_helper.h
index b498c22..ff7905a 100644
--- a/third_party/WebKit/Source/platform/scheduler/child/idle_helper.h
+++ b/third_party/WebKit/Source/platform/scheduler/child/idle_helper.h
@@ -6,6 +6,7 @@
 #define THIRD_PARTY_WEBKIT_SOURCE_PLATFORM_SCHEDULER_CHILD_IDLE_HELPER_H_
 
 #include "base/macros.h"
+#include "base/memory/weak_ptr.h"
 #include "base/message_loop/message_loop.h"
 #include "platform/PlatformExport.h"
 #include "platform/scheduler/base/task_queue_selector.h"
diff --git a/third_party/WebKit/Source/platform/scheduler/renderer/deadline_task_runner.h b/third_party/WebKit/Source/platform/scheduler/renderer/deadline_task_runner.h
index 0d3494e6..9041e00 100644
--- a/third_party/WebKit/Source/platform/scheduler/renderer/deadline_task_runner.h
+++ b/third_party/WebKit/Source/platform/scheduler/renderer/deadline_task_runner.h
@@ -7,7 +7,6 @@
 
 #include "base/callback.h"
 #include "base/macros.h"
-#include "base/memory/weak_ptr.h"
 #include "base/single_thread_task_runner.h"
 #include "base/time/time.h"
 #include "platform/PlatformExport.h"
diff --git a/third_party/WebKit/Source/platform/scheduler/renderer/renderer_scheduler_impl.h b/third_party/WebKit/Source/platform/scheduler/renderer/renderer_scheduler_impl.h
index 2dec8ce..8da453a 100644
--- a/third_party/WebKit/Source/platform/scheduler/renderer/renderer_scheduler_impl.h
+++ b/third_party/WebKit/Source/platform/scheduler/renderer/renderer_scheduler_impl.h
@@ -8,6 +8,7 @@
 #include "base/atomicops.h"
 #include "base/gtest_prod_util.h"
 #include "base/macros.h"
+#include "base/memory/weak_ptr.h"
 #include "base/message_loop/message_loop.h"
 #include "base/metrics/single_sample_metrics.h"
 #include "base/single_thread_task_runner.h"
diff --git a/third_party/WebKit/Source/platform/scheduler/renderer/task_queue_throttler.h b/third_party/WebKit/Source/platform/scheduler/renderer/task_queue_throttler.h
index ff9cb57..9d7cc850 100644
--- a/third_party/WebKit/Source/platform/scheduler/renderer/task_queue_throttler.h
+++ b/third_party/WebKit/Source/platform/scheduler/renderer/task_queue_throttler.h
@@ -10,6 +10,7 @@
 
 #include "base/logging.h"
 #include "base/macros.h"
+#include "base/memory/weak_ptr.h"
 #include "base/optional.h"
 #include "base/threading/thread_checker.h"
 #include "platform/PlatformExport.h"
diff --git a/third_party/WebKit/Source/platform/scheduler/renderer/web_view_scheduler_impl.h b/third_party/WebKit/Source/platform/scheduler/renderer/web_view_scheduler_impl.h
index ae6e4514..41cb292e 100644
--- a/third_party/WebKit/Source/platform/scheduler/renderer/web_view_scheduler_impl.h
+++ b/third_party/WebKit/Source/platform/scheduler/renderer/web_view_scheduler_impl.h
@@ -10,6 +10,7 @@
 #include <string>
 
 #include "base/macros.h"
+#include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
 #include "platform/PlatformExport.h"
 #include "platform/scheduler/base/task_queue.h"
@@ -83,8 +84,8 @@
 
   void AsValueInto(base::trace_event::TracedValue* state) const;
 
-  WTF::WeakPtr<WebViewSchedulerImpl> CreateWeakPtr() {
-    return weak_factory_.CreateWeakPtr();
+  base::WeakPtr<WebViewSchedulerImpl> GetWeakPtr() {
+    return weak_factory_.GetWeakPtr();
   }
 
  private:
@@ -116,7 +117,7 @@
   bool nested_runloop_;
   CPUTimeBudgetPool* background_time_budget_pool_;  // Not owned.
   WebViewScheduler::WebViewSchedulerDelegate* delegate_;  // Not owned.
-  WTF::WeakPtrFactory<WebViewSchedulerImpl> weak_factory_;
+  base::WeakPtrFactory<WebViewSchedulerImpl> weak_factory_;
 
   DISALLOW_COPY_AND_ASSIGN(WebViewSchedulerImpl);
 };
diff --git a/third_party/WebKit/Source/platform/scheduler/test/test_task_queue.h b/third_party/WebKit/Source/platform/scheduler/test/test_task_queue.h
index 472192c..67b10e8 100644
--- a/third_party/WebKit/Source/platform/scheduler/test/test_task_queue.h
+++ b/third_party/WebKit/Source/platform/scheduler/test/test_task_queue.h
@@ -5,6 +5,7 @@
 #ifndef THIRD_PARTY_WEBKIT_SOURCE_PLATFORM_SCHEDULER_TEST_TEST_TASK_QUEUE_H_
 #define THIRD_PARTY_WEBKIT_SOURCE_PLATFORM_SCHEDULER_TEST_TEST_TASK_QUEUE_H_
 
+#include "base/memory/weak_ptr.h"
 #include "platform/scheduler/base/task_queue.h"
 
 namespace blink {
diff --git a/third_party/WebKit/Source/platform/testing/weburl_loader_mock.cc b/third_party/WebKit/Source/platform/testing/weburl_loader_mock.cc
index f317d0e..85b0feb 100644
--- a/third_party/WebKit/Source/platform/testing/weburl_loader_mock.cc
+++ b/third_party/WebKit/Source/platform/testing/weburl_loader_mock.cc
@@ -54,7 +54,7 @@
 
   // didReceiveResponse() and didReceiveData() might end up getting ::cancel()
   // to be called which will make the ResourceLoader to delete |this|.
-  WeakPtr<WebURLLoaderMock> self = weak_factory_.CreateWeakPtr();
+  base::WeakPtr<WebURLLoaderMock> self = weak_factory_.GetWeakPtr();
 
   delegate->DidReceiveResponse(client_, response);
   if (!self)
@@ -85,7 +85,7 @@
     const WebURLResponse& redirect_response) {
   KURL redirect_url(redirect_response.HttpHeaderField("Location"));
 
-  WeakPtr<WebURLLoaderMock> self = weak_factory_.CreateWeakPtr();
+  base::WeakPtr<WebURLLoaderMock> self = weak_factory_.GetWeakPtr();
 
   bool report_raw_headers = false;
   bool follow = client_->WillFollowRedirect(
@@ -158,8 +158,8 @@
 void WebURLLoaderMock::DidChangePriority(WebURLRequest::Priority new_priority,
                                          int intra_priority_value) {}
 
-WeakPtr<WebURLLoaderMock> WebURLLoaderMock::GetWeakPtr() {
-  return weak_factory_.CreateWeakPtr();
+base::WeakPtr<WebURLLoaderMock> WebURLLoaderMock::GetWeakPtr() {
+  return weak_factory_.GetWeakPtr();
 }
 
 } // namespace blink
diff --git a/third_party/WebKit/Source/platform/testing/weburl_loader_mock.h b/third_party/WebKit/Source/platform/testing/weburl_loader_mock.h
index 12640a1..1d049ea 100644
--- a/third_party/WebKit/Source/platform/testing/weburl_loader_mock.h
+++ b/third_party/WebKit/Source/platform/testing/weburl_loader_mock.h
@@ -7,8 +7,8 @@
 
 #include <memory>
 #include "base/macros.h"
+#include "base/memory/weak_ptr.h"
 #include "platform/wtf/Optional.h"
-#include "platform/wtf/WeakPtr.h"
 #include "public/platform/WebURLError.h"
 #include "public/platform/WebURLLoader.h"
 
@@ -61,7 +61,7 @@
   bool is_deferred() { return is_deferred_; }
   bool is_cancelled() { return !client_; }
 
-  WeakPtr<WebURLLoaderMock> GetWeakPtr();
+  base::WeakPtr<WebURLLoaderMock> GetWeakPtr();
 
  private:
   WebURLLoaderMockFactoryImpl* factory_ = nullptr;
@@ -70,7 +70,7 @@
   bool using_default_loader_ = false;
   bool is_deferred_ = false;
 
-  WeakPtrFactory<WebURLLoaderMock> weak_factory_;
+  base::WeakPtrFactory<WebURLLoaderMock> weak_factory_;
 
   DISALLOW_COPY_AND_ASSIGN(WebURLLoaderMock);
 };
diff --git a/third_party/WebKit/Source/platform/testing/weburl_loader_mock_factory_impl.cc b/third_party/WebKit/Source/platform/testing/weburl_loader_mock_factory_impl.cc
index ca318b9..f3ff52f 100644
--- a/third_party/WebKit/Source/platform/testing/weburl_loader_mock_factory_impl.cc
+++ b/third_party/WebKit/Source/platform/testing/weburl_loader_mock_factory_impl.cc
@@ -111,7 +111,7 @@
   // pending_loaders_ as it might get modified.
   while (!pending_loaders_.IsEmpty()) {
     LoaderToRequestMap::iterator iter = pending_loaders_.begin();
-    WeakPtr<WebURLLoaderMock> loader(iter->key->GetWeakPtr());
+    base::WeakPtr<WebURLLoaderMock> loader(iter->key->GetWeakPtr());
     const WebURLRequest request = iter->value;
     pending_loaders_.erase(loader.get());
 
diff --git a/third_party/WebKit/Source/platform/testing/weburl_loader_mock_factory_impl.h b/third_party/WebKit/Source/platform/testing/weburl_loader_mock_factory_impl.h
index 43082d9..1739376 100644
--- a/third_party/WebKit/Source/platform/testing/weburl_loader_mock_factory_impl.h
+++ b/third_party/WebKit/Source/platform/testing/weburl_loader_mock_factory_impl.h
@@ -9,11 +9,11 @@
 
 #include "base/files/file_path.h"
 #include "base/macros.h"
+#include "base/memory/weak_ptr.h"
 #include "platform/weborigin/KURL.h"
 #include "platform/weborigin/KURLHash.h"
 #include "platform/wtf/HashMap.h"
 #include "platform/wtf/Optional.h"
-#include "platform/wtf/WeakPtr.h"
 #include "public/platform/WebURL.h"
 #include "public/platform/WebURLError.h"
 #include "public/platform/WebURLLoaderMockFactory.h"
@@ -87,7 +87,7 @@
                    WebData* data);
 
   // Checks if the loader is pending. Otherwise, it may have been deleted.
-  bool IsPending(WeakPtr<WebURLLoaderMock> loader);
+  bool IsPending(base::WeakPtr<WebURLLoaderMock> loader);
 
   // Looks up an URL in the mock URL table.
   //
diff --git a/third_party/WebKit/Tools/Scripts/audit-non-blink-usage.py b/third_party/WebKit/Tools/Scripts/audit-non-blink-usage.py
index 5d2078d9..0888d66 100755
--- a/third_party/WebKit/Tools/Scripts/audit-non-blink-usage.py
+++ b/third_party/WebKit/Tools/Scripts/audit-non-blink-usage.py
@@ -35,6 +35,8 @@
             'base::Optional',
             'base::SingleThreadTaskRunner',
             'base::UnguessableToken',
+            'base::WeakPtr',
+            'base::WeakPtrFactory',
             'base::make_optional',
             'base::make_span',
             'base::nullopt',
diff --git a/third_party/WebKit/public/platform/DEPS b/third_party/WebKit/public/platform/DEPS
index d61d0b8..a709e9e6 100644
--- a/third_party/WebKit/public/platform/DEPS
+++ b/third_party/WebKit/public/platform/DEPS
@@ -7,6 +7,7 @@
     "+base/logging.h",
     "+base/memory/ref_counted.h",
     "+base/memory/scoped_refptr.h",
+    "+base/memory/weak_ptr.h",
     "+base/metrics",
     "+base/optional.h",
     "+base/strings",
diff --git a/third_party/WebKit/public/platform/scheduler/child/single_thread_idle_task_runner.h b/third_party/WebKit/public/platform/scheduler/child/single_thread_idle_task_runner.h
index f78c237..3a4b000 100644
--- a/third_party/WebKit/public/platform/scheduler/child/single_thread_idle_task_runner.h
+++ b/third_party/WebKit/public/platform/scheduler/child/single_thread_idle_task_runner.h
@@ -11,6 +11,7 @@
 #include "base/callback.h"
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
 #include "base/single_thread_task_runner.h"
 #include "base/time/time.h"
 #include "base/trace_event/trace_event.h"
diff --git a/third_party/ink/closure/array/array.js b/third_party/ink/closure/array/array.js
new file mode 100644
index 0000000..926df8a
--- /dev/null
+++ b/third_party/ink/closure/array/array.js
@@ -0,0 +1,1667 @@
+// Copyright 2006 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 Utilities for manipulating arrays.
+ *
+ * @author pupius@google.com (Daniel Pupius)
+ * @author arv@google.com (Erik Arvidsson)
+ * @author pallosp@google.com (Peter Pallos)
+ */
+
+
+goog.provide('goog.array');
+
+goog.require('goog.asserts');
+
+
+/**
+ * @define {boolean} NATIVE_ARRAY_PROTOTYPES indicates whether the code should
+ * rely on Array.prototype functions, if available.
+ *
+ * The Array.prototype functions can be defined by external libraries like
+ * Prototype and setting this flag to false forces closure to use its own
+ * goog.array implementation.
+ *
+ * If your javascript can be loaded by a third party site and you are wary about
+ * relying on the prototype functions, specify
+ * "--define goog.NATIVE_ARRAY_PROTOTYPES=false" to the JSCompiler.
+ *
+ * Setting goog.TRUSTED_SITE to false will automatically set
+ * NATIVE_ARRAY_PROTOTYPES to false.
+ */
+goog.define('goog.NATIVE_ARRAY_PROTOTYPES', goog.TRUSTED_SITE);
+
+
+/**
+ * @define {boolean} If true, JSCompiler will use the native implementation of
+ * array functions where appropriate (e.g., {@code Array#filter}) and remove the
+ * unused pure JS implementation.
+ */
+goog.define('goog.array.ASSUME_NATIVE_FUNCTIONS', false);
+
+
+/**
+ * Returns the last element in an array without removing it.
+ * Same as goog.array.last.
+ * @param {IArrayLike<T>|string} array The array.
+ * @return {T} Last item in array.
+ * @template T
+ */
+goog.array.peek = function(array) {
+  return array[array.length - 1];
+};
+
+
+/**
+ * Returns the last element in an array without removing it.
+ * Same as goog.array.peek.
+ * @param {IArrayLike<T>|string} array The array.
+ * @return {T} Last item in array.
+ * @template T
+ */
+goog.array.last = goog.array.peek;
+
+// NOTE(arv): Since most of the array functions are generic it allows you to
+// pass an array-like object. Strings have a length and are considered array-
+// like. However, the 'in' operator does not work on strings so we cannot just
+// use the array path even if the browser supports indexing into strings. We
+// therefore end up splitting the string.
+
+
+/**
+ * Returns the index of the first element of an array with a specified value, or
+ * -1 if the element is not present in the array.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-indexof}
+ *
+ * @param {IArrayLike<T>|string} arr The array to be searched.
+ * @param {T} obj The object for which we are searching.
+ * @param {number=} opt_fromIndex The index at which to start the search. If
+ *     omitted the search starts at index 0.
+ * @return {number} The index of the first matching array element.
+ * @template T
+ */
+goog.array.indexOf = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.indexOf) ?
+    function(arr, obj, opt_fromIndex) {
+      goog.asserts.assert(arr.length != null);
+
+      return Array.prototype.indexOf.call(arr, obj, opt_fromIndex);
+    } :
+    function(arr, obj, opt_fromIndex) {
+      var fromIndex = opt_fromIndex == null ?
+          0 :
+          (opt_fromIndex < 0 ? Math.max(0, arr.length + opt_fromIndex) :
+                               opt_fromIndex);
+
+      if (goog.isString(arr)) {
+        // Array.prototype.indexOf uses === so only strings should be found.
+        if (!goog.isString(obj) || obj.length != 1) {
+          return -1;
+        }
+        return arr.indexOf(obj, fromIndex);
+      }
+
+      for (var i = fromIndex; i < arr.length; i++) {
+        if (i in arr && arr[i] === obj) return i;
+      }
+      return -1;
+    };
+
+
+/**
+ * Returns the index of the last element of an array with a specified value, or
+ * -1 if the element is not present in the array.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-lastindexof}
+ *
+ * @param {!IArrayLike<T>|string} arr The array to be searched.
+ * @param {T} obj The object for which we are searching.
+ * @param {?number=} opt_fromIndex The index at which to start the search. If
+ *     omitted the search starts at the end of the array.
+ * @return {number} The index of the last matching array element.
+ * @template T
+ */
+goog.array.lastIndexOf = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.lastIndexOf) ?
+    function(arr, obj, opt_fromIndex) {
+      goog.asserts.assert(arr.length != null);
+
+      // Firefox treats undefined and null as 0 in the fromIndex argument which
+      // leads it to always return -1
+      var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex;
+      return Array.prototype.lastIndexOf.call(arr, obj, fromIndex);
+    } :
+    function(arr, obj, opt_fromIndex) {
+      var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex;
+
+      if (fromIndex < 0) {
+        fromIndex = Math.max(0, arr.length + fromIndex);
+      }
+
+      if (goog.isString(arr)) {
+        // Array.prototype.lastIndexOf uses === so only strings should be found.
+        if (!goog.isString(obj) || obj.length != 1) {
+          return -1;
+        }
+        return arr.lastIndexOf(obj, fromIndex);
+      }
+
+      for (var i = fromIndex; i >= 0; i--) {
+        if (i in arr && arr[i] === obj) return i;
+      }
+      return -1;
+    };
+
+
+/**
+ * Calls a function for each element in an array. Skips holes in the array.
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-foreach}
+ *
+ * @param {IArrayLike<T>|string} arr Array or array like object over
+ *     which to iterate.
+ * @param {?function(this: S, T, number, ?): ?} f The function to call for every
+ *     element. This function takes 3 arguments (the element, the index and the
+ *     array). The return value is ignored.
+ * @param {S=} opt_obj The object to be used as the value of 'this' within f.
+ * @template T,S
+ */
+goog.array.forEach = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.forEach) ?
+    function(arr, f, opt_obj) {
+      goog.asserts.assert(arr.length != null);
+
+      Array.prototype.forEach.call(arr, f, opt_obj);
+    } :
+    function(arr, f, opt_obj) {
+      var l = arr.length;  // must be fixed during loop... see docs
+      var arr2 = goog.isString(arr) ? arr.split('') : arr;
+      for (var i = 0; i < l; i++) {
+        if (i in arr2) {
+          f.call(/** @type {?} */ (opt_obj), arr2[i], i, arr);
+        }
+      }
+    };
+
+
+/**
+ * Calls a function for each element in an array, starting from the last
+ * element rather than the first.
+ *
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this: S, T, number, ?): ?} f The function to call for every
+ *     element. This function
+ *     takes 3 arguments (the element, the index and the array). The return
+ *     value is ignored.
+ * @param {S=} opt_obj The object to be used as the value of 'this'
+ *     within f.
+ * @template T,S
+ */
+goog.array.forEachRight = function(arr, f, opt_obj) {
+  var l = arr.length;  // must be fixed during loop... see docs
+  var arr2 = goog.isString(arr) ? arr.split('') : arr;
+  for (var i = l - 1; i >= 0; --i) {
+    if (i in arr2) {
+      f.call(/** @type {?} */ (opt_obj), arr2[i], i, arr);
+    }
+  }
+};
+
+
+/**
+ * Calls a function for each element in an array, and if the function returns
+ * true adds the element to a new array.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-filter}
+ *
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?):boolean} f The function to call for
+ *     every element. This function
+ *     takes 3 arguments (the element, the index and the array) and must
+ *     return a Boolean. If the return value is true the element is added to the
+ *     result array. If it is false the element is not included.
+ * @param {S=} opt_obj The object to be used as the value of 'this'
+ *     within f.
+ * @return {!Array<T>} a new array in which only elements that passed the test
+ *     are present.
+ * @template T,S
+ */
+goog.array.filter = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.filter) ?
+    function(arr, f, opt_obj) {
+      goog.asserts.assert(arr.length != null);
+
+      return Array.prototype.filter.call(arr, f, opt_obj);
+    } :
+    function(arr, f, opt_obj) {
+      var l = arr.length;  // must be fixed during loop... see docs
+      var res = [];
+      var resLength = 0;
+      var arr2 = goog.isString(arr) ? arr.split('') : arr;
+      for (var i = 0; i < l; i++) {
+        if (i in arr2) {
+          var val = arr2[i];  // in case f mutates arr2
+          if (f.call(/** @type {?} */ (opt_obj), val, i, arr)) {
+            res[resLength++] = val;
+          }
+        }
+      }
+      return res;
+    };
+
+
+/**
+ * Calls a function for each element in an array and inserts the result into a
+ * new array.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-map}
+ *
+ * @param {IArrayLike<VALUE>|string} arr Array or array like object
+ *     over which to iterate.
+ * @param {function(this:THIS, VALUE, number, ?): RESULT} f The function to call
+ *     for every element. This function takes 3 arguments (the element,
+ *     the index and the array) and should return something. The result will be
+ *     inserted into a new array.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within f.
+ * @return {!Array<RESULT>} a new array with the results from f.
+ * @template THIS, VALUE, RESULT
+ */
+goog.array.map = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.map) ?
+    function(arr, f, opt_obj) {
+      goog.asserts.assert(arr.length != null);
+
+      return Array.prototype.map.call(arr, f, opt_obj);
+    } :
+    function(arr, f, opt_obj) {
+      var l = arr.length;  // must be fixed during loop... see docs
+      var res = new Array(l);
+      var arr2 = goog.isString(arr) ? arr.split('') : arr;
+      for (var i = 0; i < l; i++) {
+        if (i in arr2) {
+          res[i] = f.call(/** @type {?} */ (opt_obj), arr2[i], i, arr);
+        }
+      }
+      return res;
+    };
+
+
+/**
+ * Passes every element of an array into a function and accumulates the result.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-reduce}
+ *
+ * For example:
+ * var a = [1, 2, 3, 4];
+ * goog.array.reduce(a, function(r, v, i, arr) {return r + v;}, 0);
+ * returns 10
+ *
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {function(this:S, R, T, number, ?) : R} f The function to call for
+ *     every element. This function
+ *     takes 4 arguments (the function's previous result or the initial value,
+ *     the value of the current array element, the current array index, and the
+ *     array itself)
+ *     function(previousValue, currentValue, index, array).
+ * @param {?} val The initial value to pass into the function on the first call.
+ * @param {S=} opt_obj  The object to be used as the value of 'this'
+ *     within f.
+ * @return {R} Result of evaluating f repeatedly across the values of the array.
+ * @template T,S,R
+ */
+goog.array.reduce = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.reduce) ?
+    function(arr, f, val, opt_obj) {
+      goog.asserts.assert(arr.length != null);
+      if (opt_obj) {
+        f = goog.bind(f, opt_obj);
+      }
+      return Array.prototype.reduce.call(arr, f, val);
+    } :
+    function(arr, f, val, opt_obj) {
+      var rval = val;
+      goog.array.forEach(arr, function(val, index) {
+        rval = f.call(/** @type {?} */ (opt_obj), rval, val, index, arr);
+      });
+      return rval;
+    };
+
+
+/**
+ * Passes every element of an array into a function and accumulates the result,
+ * starting from the last element and working towards the first.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-reduceright}
+ *
+ * For example:
+ * var a = ['a', 'b', 'c'];
+ * goog.array.reduceRight(a, function(r, v, i, arr) {return r + v;}, '');
+ * returns 'cba'
+ *
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, R, T, number, ?) : R} f The function to call for
+ *     every element. This function
+ *     takes 4 arguments (the function's previous result or the initial value,
+ *     the value of the current array element, the current array index, and the
+ *     array itself)
+ *     function(previousValue, currentValue, index, array).
+ * @param {?} val The initial value to pass into the function on the first call.
+ * @param {S=} opt_obj The object to be used as the value of 'this'
+ *     within f.
+ * @return {R} Object returned as a result of evaluating f repeatedly across the
+ *     values of the array.
+ * @template T,S,R
+ */
+goog.array.reduceRight = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.reduceRight) ?
+    function(arr, f, val, opt_obj) {
+      goog.asserts.assert(arr.length != null);
+      goog.asserts.assert(f != null);
+      if (opt_obj) {
+        f = goog.bind(f, opt_obj);
+      }
+      return Array.prototype.reduceRight.call(arr, f, val);
+    } :
+    function(arr, f, val, opt_obj) {
+      var rval = val;
+      goog.array.forEachRight(arr, function(val, index) {
+        rval = f.call(/** @type {?} */ (opt_obj), rval, val, index, arr);
+      });
+      return rval;
+    };
+
+
+/**
+ * Calls f for each element of an array. If any call returns true, some()
+ * returns true (without checking the remaining elements). If all calls
+ * return false, some() returns false.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-some}
+ *
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call for
+ *     for every element. This function takes 3 arguments (the element, the
+ *     index and the array) and should return a boolean.
+ * @param {S=} opt_obj  The object to be used as the value of 'this'
+ *     within f.
+ * @return {boolean} true if any element passes the test.
+ * @template T,S
+ */
+goog.array.some = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.some) ?
+    function(arr, f, opt_obj) {
+      goog.asserts.assert(arr.length != null);
+
+      return Array.prototype.some.call(arr, f, opt_obj);
+    } :
+    function(arr, f, opt_obj) {
+      var l = arr.length;  // must be fixed during loop... see docs
+      var arr2 = goog.isString(arr) ? arr.split('') : arr;
+      for (var i = 0; i < l; i++) {
+        if (i in arr2 && f.call(/** @type {?} */ (opt_obj), arr2[i], i, arr)) {
+          return true;
+        }
+      }
+      return false;
+    };
+
+
+/**
+ * Call f for each element of an array. If all calls return true, every()
+ * returns true. If any call returns false, every() returns false and
+ * does not continue to check the remaining elements.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-every}
+ *
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call for
+ *     for every element. This function takes 3 arguments (the element, the
+ *     index and the array) and should return a boolean.
+ * @param {S=} opt_obj The object to be used as the value of 'this'
+ *     within f.
+ * @return {boolean} false if any element fails the test.
+ * @template T,S
+ */
+goog.array.every = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.every) ?
+    function(arr, f, opt_obj) {
+      goog.asserts.assert(arr.length != null);
+
+      return Array.prototype.every.call(arr, f, opt_obj);
+    } :
+    function(arr, f, opt_obj) {
+      var l = arr.length;  // must be fixed during loop... see docs
+      var arr2 = goog.isString(arr) ? arr.split('') : arr;
+      for (var i = 0; i < l; i++) {
+        if (i in arr2 && !f.call(/** @type {?} */ (opt_obj), arr2[i], i, arr)) {
+          return false;
+        }
+      }
+      return true;
+    };
+
+
+/**
+ * Counts the array elements that fulfill the predicate, i.e. for which the
+ * callback function returns true. Skips holes in the array.
+ *
+ * @param {!IArrayLike<T>|string} arr Array or array like object
+ *     over which to iterate.
+ * @param {function(this: S, T, number, ?): boolean} f The function to call for
+ *     every element. Takes 3 arguments (the element, the index and the array).
+ * @param {S=} opt_obj The object to be used as the value of 'this' within f.
+ * @return {number} The number of the matching elements.
+ * @template T,S
+ */
+goog.array.count = function(arr, f, opt_obj) {
+  var count = 0;
+  goog.array.forEach(arr, function(element, index, arr) {
+    if (f.call(/** @type {?} */ (opt_obj), element, index, arr)) {
+      ++count;
+    }
+  }, opt_obj);
+  return count;
+};
+
+
+/**
+ * Search an array for the first element that satisfies a given condition and
+ * return that element.
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call
+ *     for every element. This function takes 3 arguments (the element, the
+ *     index and the array) and should return a boolean.
+ * @param {S=} opt_obj An optional "this" context for the function.
+ * @return {T|null} The first array element that passes the test, or null if no
+ *     element is found.
+ * @template T,S
+ */
+goog.array.find = function(arr, f, opt_obj) {
+  var i = goog.array.findIndex(arr, f, opt_obj);
+  return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i];
+};
+
+
+/**
+ * Search an array for the first element that satisfies a given condition and
+ * return its index.
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call for
+ *     every element. This function
+ *     takes 3 arguments (the element, the index and the array) and should
+ *     return a boolean.
+ * @param {S=} opt_obj An optional "this" context for the function.
+ * @return {number} The index of the first array element that passes the test,
+ *     or -1 if no element is found.
+ * @template T,S
+ */
+goog.array.findIndex = function(arr, f, opt_obj) {
+  var l = arr.length;  // must be fixed during loop... see docs
+  var arr2 = goog.isString(arr) ? arr.split('') : arr;
+  for (var i = 0; i < l; i++) {
+    if (i in arr2 && f.call(/** @type {?} */ (opt_obj), arr2[i], i, arr)) {
+      return i;
+    }
+  }
+  return -1;
+};
+
+
+/**
+ * Search an array (in reverse order) for the last element that satisfies a
+ * given condition and return that element.
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call
+ *     for every element. This function
+ *     takes 3 arguments (the element, the index and the array) and should
+ *     return a boolean.
+ * @param {S=} opt_obj An optional "this" context for the function.
+ * @return {T|null} The last array element that passes the test, or null if no
+ *     element is found.
+ * @template T,S
+ */
+goog.array.findRight = function(arr, f, opt_obj) {
+  var i = goog.array.findIndexRight(arr, f, opt_obj);
+  return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i];
+};
+
+
+/**
+ * Search an array (in reverse order) for the last element that satisfies a
+ * given condition and return its index.
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call
+ *     for every element. This function
+ *     takes 3 arguments (the element, the index and the array) and should
+ *     return a boolean.
+ * @param {S=} opt_obj An optional "this" context for the function.
+ * @return {number} The index of the last array element that passes the test,
+ *     or -1 if no element is found.
+ * @template T,S
+ */
+goog.array.findIndexRight = function(arr, f, opt_obj) {
+  var l = arr.length;  // must be fixed during loop... see docs
+  var arr2 = goog.isString(arr) ? arr.split('') : arr;
+  for (var i = l - 1; i >= 0; i--) {
+    if (i in arr2 && f.call(/** @type {?} */ (opt_obj), arr2[i], i, arr)) {
+      return i;
+    }
+  }
+  return -1;
+};
+
+
+/**
+ * Whether the array contains the given object.
+ * @param {IArrayLike<?>|string} arr The array to test for the presence of the
+ *     element.
+ * @param {*} obj The object for which to test.
+ * @return {boolean} true if obj is present.
+ */
+goog.array.contains = function(arr, obj) {
+  return goog.array.indexOf(arr, obj) >= 0;
+};
+
+
+/**
+ * Whether the array is empty.
+ * @param {IArrayLike<?>|string} arr The array to test.
+ * @return {boolean} true if empty.
+ */
+goog.array.isEmpty = function(arr) {
+  return arr.length == 0;
+};
+
+
+/**
+ * Clears the array.
+ * @param {IArrayLike<?>} arr Array or array like object to clear.
+ */
+goog.array.clear = function(arr) {
+  // For non real arrays we don't have the magic length so we delete the
+  // indices.
+  if (!goog.isArray(arr)) {
+    for (var i = arr.length - 1; i >= 0; i--) {
+      delete arr[i];
+    }
+  }
+  arr.length = 0;
+};
+
+
+/**
+ * Pushes an item into an array, if it's not already in the array.
+ * @param {Array<T>} arr Array into which to insert the item.
+ * @param {T} obj Value to add.
+ * @template T
+ */
+goog.array.insert = function(arr, obj) {
+  if (!goog.array.contains(arr, obj)) {
+    arr.push(obj);
+  }
+};
+
+
+/**
+ * Inserts an object at the given index of the array.
+ * @param {IArrayLike<?>} arr The array to modify.
+ * @param {*} obj The object to insert.
+ * @param {number=} opt_i The index at which to insert the object. If omitted,
+ *      treated as 0. A negative index is counted from the end of the array.
+ */
+goog.array.insertAt = function(arr, obj, opt_i) {
+  goog.array.splice(arr, opt_i, 0, obj);
+};
+
+
+/**
+ * Inserts at the given index of the array, all elements of another array.
+ * @param {IArrayLike<?>} arr The array to modify.
+ * @param {IArrayLike<?>} elementsToAdd The array of elements to add.
+ * @param {number=} opt_i The index at which to insert the object. If omitted,
+ *      treated as 0. A negative index is counted from the end of the array.
+ */
+goog.array.insertArrayAt = function(arr, elementsToAdd, opt_i) {
+  goog.partial(goog.array.splice, arr, opt_i, 0).apply(null, elementsToAdd);
+};
+
+
+/**
+ * Inserts an object into an array before a specified object.
+ * @param {Array<T>} arr The array to modify.
+ * @param {T} obj The object to insert.
+ * @param {T=} opt_obj2 The object before which obj should be inserted. If obj2
+ *     is omitted or not found, obj is inserted at the end of the array.
+ * @template T
+ */
+goog.array.insertBefore = function(arr, obj, opt_obj2) {
+  var i;
+  if (arguments.length == 2 || (i = goog.array.indexOf(arr, opt_obj2)) < 0) {
+    arr.push(obj);
+  } else {
+    goog.array.insertAt(arr, obj, i);
+  }
+};
+
+
+/**
+ * Removes the first occurrence of a particular value from an array.
+ * @param {IArrayLike<T>} arr Array from which to remove
+ *     value.
+ * @param {T} obj Object to remove.
+ * @return {boolean} True if an element was removed.
+ * @template T
+ */
+goog.array.remove = function(arr, obj) {
+  var i = goog.array.indexOf(arr, obj);
+  var rv;
+  if ((rv = i >= 0)) {
+    goog.array.removeAt(arr, i);
+  }
+  return rv;
+};
+
+
+/**
+ * Removes the last occurrence of a particular value from an array.
+ * @param {!IArrayLike<T>} arr Array from which to remove value.
+ * @param {T} obj Object to remove.
+ * @return {boolean} True if an element was removed.
+ * @template T
+ */
+goog.array.removeLast = function(arr, obj) {
+  var i = goog.array.lastIndexOf(arr, obj);
+  if (i >= 0) {
+    goog.array.removeAt(arr, i);
+    return true;
+  }
+  return false;
+};
+
+
+/**
+ * Removes from an array the element at index i
+ * @param {IArrayLike<?>} arr Array or array like object from which to
+ *     remove value.
+ * @param {number} i The index to remove.
+ * @return {boolean} True if an element was removed.
+ */
+goog.array.removeAt = function(arr, i) {
+  goog.asserts.assert(arr.length != null);
+
+  // use generic form of splice
+  // splice returns the removed items and if successful the length of that
+  // will be 1
+  return Array.prototype.splice.call(arr, i, 1).length == 1;
+};
+
+
+/**
+ * Removes the first value that satisfies the given condition.
+ * @param {IArrayLike<T>} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call
+ *     for every element. This function
+ *     takes 3 arguments (the element, the index and the array) and should
+ *     return a boolean.
+ * @param {S=} opt_obj An optional "this" context for the function.
+ * @return {boolean} True if an element was removed.
+ * @template T,S
+ */
+goog.array.removeIf = function(arr, f, opt_obj) {
+  var i = goog.array.findIndex(arr, f, opt_obj);
+  if (i >= 0) {
+    goog.array.removeAt(arr, i);
+    return true;
+  }
+  return false;
+};
+
+
+/**
+ * Removes all values that satisfy the given condition.
+ * @param {IArrayLike<T>} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call
+ *     for every element. This function
+ *     takes 3 arguments (the element, the index and the array) and should
+ *     return a boolean.
+ * @param {S=} opt_obj An optional "this" context for the function.
+ * @return {number} The number of items removed
+ * @template T,S
+ */
+goog.array.removeAllIf = function(arr, f, opt_obj) {
+  var removedCount = 0;
+  goog.array.forEachRight(arr, function(val, index) {
+    if (f.call(/** @type {?} */ (opt_obj), val, index, arr)) {
+      if (goog.array.removeAt(arr, index)) {
+        removedCount++;
+      }
+    }
+  });
+  return removedCount;
+};
+
+
+/**
+ * Returns a new array that is the result of joining the arguments.  If arrays
+ * are passed then their items are added, however, if non-arrays are passed they
+ * will be added to the return array as is.
+ *
+ * Note that ArrayLike objects will be added as is, rather than having their
+ * items added.
+ *
+ * goog.array.concat([1, 2], [3, 4]) -> [1, 2, 3, 4]
+ * goog.array.concat(0, [1, 2]) -> [0, 1, 2]
+ * goog.array.concat([1, 2], null) -> [1, 2, null]
+ *
+ * There is bug in all current versions of IE (6, 7 and 8) where arrays created
+ * in an iframe become corrupted soon (not immediately) after the iframe is
+ * destroyed. This is common if loading data via goog.net.IframeIo, for example.
+ * This corruption only affects the concat method which will start throwing
+ * Catastrophic Errors (#-2147418113).
+ *
+ * See http://endoflow.com/scratch/corrupted-arrays.html for a test case.
+ *
+ * Internally goog.array should use this, so that all methods will continue to
+ * work on these broken array objects.
+ *
+ * @param {...*} var_args Items to concatenate.  Arrays will have each item
+ *     added, while primitives and objects will be added as is.
+ * @return {!Array<?>} The new resultant array.
+ */
+goog.array.concat = function(var_args) {
+  return Array.prototype.concat.apply([], arguments);
+};
+
+
+/**
+ * Returns a new array that contains the contents of all the arrays passed.
+ * @param {...!Array<T>} var_args
+ * @return {!Array<T>}
+ * @template T
+ */
+goog.array.join = function(var_args) {
+  return Array.prototype.concat.apply([], arguments);
+};
+
+
+/**
+ * Converts an object to an array.
+ * @param {IArrayLike<T>|string} object  The object to convert to an
+ *     array.
+ * @return {!Array<T>} The object converted into an array. If object has a
+ *     length property, every property indexed with a non-negative number
+ *     less than length will be included in the result. If object does not
+ *     have a length property, an empty array will be returned.
+ * @template T
+ */
+goog.array.toArray = function(object) {
+  var length = object.length;
+
+  // If length is not a number the following it false. This case is kept for
+  // backwards compatibility since there are callers that pass objects that are
+  // not array like.
+  if (length > 0) {
+    var rv = new Array(length);
+    for (var i = 0; i < length; i++) {
+      rv[i] = object[i];
+    }
+    return rv;
+  }
+  return [];
+};
+
+
+/**
+ * Does a shallow copy of an array.
+ * @param {IArrayLike<T>|string} arr  Array or array-like object to
+ *     clone.
+ * @return {!Array<T>} Clone of the input array.
+ * @template T
+ */
+goog.array.clone = goog.array.toArray;
+
+
+/**
+ * Extends an array with another array, element, or "array like" object.
+ * This function operates 'in-place', it does not create a new Array.
+ *
+ * Example:
+ * var a = [];
+ * goog.array.extend(a, [0, 1]);
+ * a; // [0, 1]
+ * goog.array.extend(a, 2);
+ * a; // [0, 1, 2]
+ *
+ * @param {Array<VALUE>} arr1  The array to modify.
+ * @param {...(Array<VALUE>|VALUE)} var_args The elements or arrays of elements
+ *     to add to arr1.
+ * @template VALUE
+ */
+goog.array.extend = function(arr1, var_args) {
+  for (var i = 1; i < arguments.length; i++) {
+    var arr2 = arguments[i];
+    if (goog.isArrayLike(arr2)) {
+      var len1 = arr1.length || 0;
+      var len2 = arr2.length || 0;
+      arr1.length = len1 + len2;
+      for (var j = 0; j < len2; j++) {
+        arr1[len1 + j] = arr2[j];
+      }
+    } else {
+      arr1.push(arr2);
+    }
+  }
+};
+
+
+/**
+ * Adds or removes elements from an array. This is a generic version of Array
+ * splice. This means that it might work on other objects similar to arrays,
+ * such as the arguments object.
+ *
+ * @param {IArrayLike<T>} arr The array to modify.
+ * @param {number|undefined} index The index at which to start changing the
+ *     array. If not defined, treated as 0.
+ * @param {number} howMany How many elements to remove (0 means no removal. A
+ *     value below 0 is treated as zero and so is any other non number. Numbers
+ *     are floored).
+ * @param {...T} var_args Optional, additional elements to insert into the
+ *     array.
+ * @return {!Array<T>} the removed elements.
+ * @template T
+ */
+goog.array.splice = function(arr, index, howMany, var_args) {
+  goog.asserts.assert(arr.length != null);
+
+  return Array.prototype.splice.apply(arr, goog.array.slice(arguments, 1));
+};
+
+
+/**
+ * Returns a new array from a segment of an array. This is a generic version of
+ * Array slice. This means that it might work on other objects similar to
+ * arrays, such as the arguments object.
+ *
+ * @param {IArrayLike<T>|string} arr The array from
+ * which to copy a segment.
+ * @param {number} start The index of the first element to copy.
+ * @param {number=} opt_end The index after the last element to copy.
+ * @return {!Array<T>} A new array containing the specified segment of the
+ *     original array.
+ * @template T
+ */
+goog.array.slice = function(arr, start, opt_end) {
+  goog.asserts.assert(arr.length != null);
+
+  // passing 1 arg to slice is not the same as passing 2 where the second is
+  // null or undefined (in that case the second argument is treated as 0).
+  // we could use slice on the arguments object and then use apply instead of
+  // testing the length
+  if (arguments.length <= 2) {
+    return Array.prototype.slice.call(arr, start);
+  } else {
+    return Array.prototype.slice.call(arr, start, opt_end);
+  }
+};
+
+
+/**
+ * Removes all duplicates from an array (retaining only the first
+ * occurrence of each array element).  This function modifies the
+ * array in place and doesn't change the order of the non-duplicate items.
+ *
+ * For objects, duplicates are identified as having the same unique ID as
+ * defined by {@link goog.getUid}.
+ *
+ * Alternatively you can specify a custom hash function that returns a unique
+ * value for each item in the array it should consider unique.
+ *
+ * Runtime: N,
+ * Worstcase space: 2N (no dupes)
+ *
+ * @param {IArrayLike<T>} arr The array from which to remove
+ *     duplicates.
+ * @param {Array=} opt_rv An optional array in which to return the results,
+ *     instead of performing the removal inplace.  If specified, the original
+ *     array will remain unchanged.
+ * @param {function(T):string=} opt_hashFn An optional function to use to
+ *     apply to every item in the array. This function should return a unique
+ *     value for each item in the array it should consider unique.
+ * @template T
+ */
+goog.array.removeDuplicates = function(arr, opt_rv, opt_hashFn) {
+  var returnArray = opt_rv || arr;
+  var defaultHashFn = function(item) {
+    // Prefix each type with a single character representing the type to
+    // prevent conflicting keys (e.g. true and 'true').
+    return goog.isObject(item) ? 'o' + goog.getUid(item) :
+                                 (typeof item).charAt(0) + item;
+  };
+  var hashFn = opt_hashFn || defaultHashFn;
+
+  var seen = {}, cursorInsert = 0, cursorRead = 0;
+  while (cursorRead < arr.length) {
+    var current = arr[cursorRead++];
+    var key = hashFn(current);
+    if (!Object.prototype.hasOwnProperty.call(seen, key)) {
+      seen[key] = true;
+      returnArray[cursorInsert++] = current;
+    }
+  }
+  returnArray.length = cursorInsert;
+};
+
+
+/**
+ * Searches the specified array for the specified target using the binary
+ * search algorithm.  If no opt_compareFn is specified, elements are compared
+ * using <code>goog.array.defaultCompare</code>, which compares the elements
+ * using the built in < and > operators.  This will produce the expected
+ * behavior for homogeneous arrays of String(s) and Number(s). The array
+ * specified <b>must</b> be sorted in ascending order (as defined by the
+ * comparison function).  If the array is not sorted, results are undefined.
+ * If the array contains multiple instances of the specified target value, any
+ * of these instances may be found.
+ *
+ * Runtime: O(log n)
+ *
+ * @param {IArrayLike<VALUE>} arr The array to be searched.
+ * @param {TARGET} target The sought value.
+ * @param {function(TARGET, VALUE): number=} opt_compareFn Optional comparison
+ *     function by which the array is ordered. Should take 2 arguments to
+ *     compare, and return a negative number, zero, or a positive number
+ *     depending on whether the first argument is less than, equal to, or
+ *     greater than the second.
+ * @return {number} Lowest index of the target value if found, otherwise
+ *     (-(insertion point) - 1). The insertion point is where the value should
+ *     be inserted into arr to preserve the sorted property.  Return value >= 0
+ *     iff target is found.
+ * @template TARGET, VALUE
+ */
+goog.array.binarySearch = function(arr, target, opt_compareFn) {
+  return goog.array.binarySearch_(
+      arr, opt_compareFn || goog.array.defaultCompare, false /* isEvaluator */,
+      target);
+};
+
+
+/**
+ * Selects an index in the specified array using the binary search algorithm.
+ * The evaluator receives an element and determines whether the desired index
+ * is before, at, or after it.  The evaluator must be consistent (formally,
+ * goog.array.map(goog.array.map(arr, evaluator, opt_obj), goog.math.sign)
+ * must be monotonically non-increasing).
+ *
+ * Runtime: O(log n)
+ *
+ * @param {IArrayLike<VALUE>} arr The array to be searched.
+ * @param {function(this:THIS, VALUE, number, ?): number} evaluator
+ *     Evaluator function that receives 3 arguments (the element, the index and
+ *     the array). Should return a negative number, zero, or a positive number
+ *     depending on whether the desired index is before, at, or after the
+ *     element passed to it.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this'
+ *     within evaluator.
+ * @return {number} Index of the leftmost element matched by the evaluator, if
+ *     such exists; otherwise (-(insertion point) - 1). The insertion point is
+ *     the index of the first element for which the evaluator returns negative,
+ *     or arr.length if no such element exists. The return value is non-negative
+ *     iff a match is found.
+ * @template THIS, VALUE
+ */
+goog.array.binarySelect = function(arr, evaluator, opt_obj) {
+  return goog.array.binarySearch_(
+      arr, evaluator, true /* isEvaluator */, undefined /* opt_target */,
+      opt_obj);
+};
+
+
+/**
+ * Implementation of a binary search algorithm which knows how to use both
+ * comparison functions and evaluators. If an evaluator is provided, will call
+ * the evaluator with the given optional data object, conforming to the
+ * interface defined in binarySelect. Otherwise, if a comparison function is
+ * provided, will call the comparison function against the given data object.
+ *
+ * This implementation purposefully does not use goog.bind or goog.partial for
+ * performance reasons.
+ *
+ * Runtime: O(log n)
+ *
+ * @param {IArrayLike<?>} arr The array to be searched.
+ * @param {function(?, ?, ?): number | function(?, ?): number} compareFn
+ *     Either an evaluator or a comparison function, as defined by binarySearch
+ *     and binarySelect above.
+ * @param {boolean} isEvaluator Whether the function is an evaluator or a
+ *     comparison function.
+ * @param {?=} opt_target If the function is a comparison function, then
+ *     this is the target to binary search for.
+ * @param {Object=} opt_selfObj If the function is an evaluator, this is an
+ *     optional this object for the evaluator.
+ * @return {number} Lowest index of the target value if found, otherwise
+ *     (-(insertion point) - 1). The insertion point is where the value should
+ *     be inserted into arr to preserve the sorted property.  Return value >= 0
+ *     iff target is found.
+ * @private
+ */
+goog.array.binarySearch_ = function(
+    arr, compareFn, isEvaluator, opt_target, opt_selfObj) {
+  var left = 0;            // inclusive
+  var right = arr.length;  // exclusive
+  var found;
+  while (left < right) {
+    var middle = (left + right) >> 1;
+    var compareResult;
+    if (isEvaluator) {
+      compareResult = compareFn.call(opt_selfObj, arr[middle], middle, arr);
+    } else {
+      // NOTE(dimvar): To avoid this cast, we'd have to use function overloading
+      // for the type of binarySearch_, which the type system can't express yet.
+      compareResult = /** @type {function(?, ?): number} */ (compareFn)(
+          opt_target, arr[middle]);
+    }
+    if (compareResult > 0) {
+      left = middle + 1;
+    } else {
+      right = middle;
+      // We are looking for the lowest index so we can't return immediately.
+      found = !compareResult;
+    }
+  }
+  // left is the index if found, or the insertion point otherwise.
+  // ~left is a shorthand for -left - 1.
+  return found ? left : ~left;
+};
+
+
+/**
+ * Sorts the specified array into ascending order.  If no opt_compareFn is
+ * specified, elements are compared using
+ * <code>goog.array.defaultCompare</code>, which compares the elements using
+ * the built in < and > operators.  This will produce the expected behavior
+ * for homogeneous arrays of String(s) and Number(s), unlike the native sort,
+ * but will give unpredictable results for heterogeneous lists of strings and
+ * numbers with different numbers of digits.
+ *
+ * This sort is not guaranteed to be stable.
+ *
+ * Runtime: Same as <code>Array.prototype.sort</code>
+ *
+ * @param {Array<T>} arr The array to be sorted.
+ * @param {?function(T,T):number=} opt_compareFn Optional comparison
+ *     function by which the
+ *     array is to be ordered. Should take 2 arguments to compare, and return a
+ *     negative number, zero, or a positive number depending on whether the
+ *     first argument is less than, equal to, or greater than the second.
+ * @template T
+ */
+goog.array.sort = function(arr, opt_compareFn) {
+  // TODO(arv): Update type annotation since null is not accepted.
+  arr.sort(opt_compareFn || goog.array.defaultCompare);
+};
+
+
+/**
+ * Sorts the specified array into ascending order in a stable way.  If no
+ * opt_compareFn is specified, elements are compared using
+ * <code>goog.array.defaultCompare</code>, which compares the elements using
+ * the built in < and > operators.  This will produce the expected behavior
+ * for homogeneous arrays of String(s) and Number(s).
+ *
+ * Runtime: Same as <code>Array.prototype.sort</code>, plus an additional
+ * O(n) overhead of copying the array twice.
+ *
+ * @param {Array<T>} arr The array to be sorted.
+ * @param {?function(T, T): number=} opt_compareFn Optional comparison function
+ *     by which the array is to be ordered. Should take 2 arguments to compare,
+ *     and return a negative number, zero, or a positive number depending on
+ *     whether the first argument is less than, equal to, or greater than the
+ *     second.
+ * @template T
+ */
+goog.array.stableSort = function(arr, opt_compareFn) {
+  var compArr = new Array(arr.length);
+  for (var i = 0; i < arr.length; i++) {
+    compArr[i] = {index: i, value: arr[i]};
+  }
+  var valueCompareFn = opt_compareFn || goog.array.defaultCompare;
+  function stableCompareFn(obj1, obj2) {
+    return valueCompareFn(obj1.value, obj2.value) || obj1.index - obj2.index;
+  }
+  goog.array.sort(compArr, stableCompareFn);
+  for (var i = 0; i < arr.length; i++) {
+    arr[i] = compArr[i].value;
+  }
+};
+
+
+/**
+ * Sort the specified array into ascending order based on item keys
+ * returned by the specified key function.
+ * If no opt_compareFn is specified, the keys are compared in ascending order
+ * using <code>goog.array.defaultCompare</code>.
+ *
+ * Runtime: O(S(f(n)), where S is runtime of <code>goog.array.sort</code>
+ * and f(n) is runtime of the key function.
+ *
+ * @param {Array<T>} arr The array to be sorted.
+ * @param {function(T): K} keyFn Function taking array element and returning
+ *     a key used for sorting this element.
+ * @param {?function(K, K): number=} opt_compareFn Optional comparison function
+ *     by which the keys are to be ordered. Should take 2 arguments to compare,
+ *     and return a negative number, zero, or a positive number depending on
+ *     whether the first argument is less than, equal to, or greater than the
+ *     second.
+ * @template T,K
+ */
+goog.array.sortByKey = function(arr, keyFn, opt_compareFn) {
+  var keyCompareFn = opt_compareFn || goog.array.defaultCompare;
+  goog.array.sort(
+      arr, function(a, b) { return keyCompareFn(keyFn(a), keyFn(b)); });
+};
+
+
+/**
+ * Sorts an array of objects by the specified object key and compare
+ * function. If no compare function is provided, the key values are
+ * compared in ascending order using <code>goog.array.defaultCompare</code>.
+ * This won't work for keys that get renamed by the compiler. So use
+ * {'foo': 1, 'bar': 2} rather than {foo: 1, bar: 2}.
+ * @param {Array<Object>} arr An array of objects to sort.
+ * @param {string} key The object key to sort by.
+ * @param {Function=} opt_compareFn The function to use to compare key
+ *     values.
+ */
+goog.array.sortObjectsByKey = function(arr, key, opt_compareFn) {
+  goog.array.sortByKey(arr, function(obj) { return obj[key]; }, opt_compareFn);
+};
+
+
+/**
+ * Tells if the array is sorted.
+ * @param {!Array<T>} arr The array.
+ * @param {?function(T,T):number=} opt_compareFn Function to compare the
+ *     array elements.
+ *     Should take 2 arguments to compare, and return a negative number, zero,
+ *     or a positive number depending on whether the first argument is less
+ *     than, equal to, or greater than the second.
+ * @param {boolean=} opt_strict If true no equal elements are allowed.
+ * @return {boolean} Whether the array is sorted.
+ * @template T
+ */
+goog.array.isSorted = function(arr, opt_compareFn, opt_strict) {
+  var compare = opt_compareFn || goog.array.defaultCompare;
+  for (var i = 1; i < arr.length; i++) {
+    var compareResult = compare(arr[i - 1], arr[i]);
+    if (compareResult > 0 || compareResult == 0 && opt_strict) {
+      return false;
+    }
+  }
+  return true;
+};
+
+
+/**
+ * Compares two arrays for equality. Two arrays are considered equal if they
+ * have the same length and their corresponding elements are equal according to
+ * the comparison function.
+ *
+ * @param {IArrayLike<?>} arr1 The first array to compare.
+ * @param {IArrayLike<?>} arr2 The second array to compare.
+ * @param {Function=} opt_equalsFn Optional comparison function.
+ *     Should take 2 arguments to compare, and return true if the arguments
+ *     are equal. Defaults to {@link goog.array.defaultCompareEquality} which
+ *     compares the elements using the built-in '===' operator.
+ * @return {boolean} Whether the two arrays are equal.
+ */
+goog.array.equals = function(arr1, arr2, opt_equalsFn) {
+  if (!goog.isArrayLike(arr1) || !goog.isArrayLike(arr2) ||
+      arr1.length != arr2.length) {
+    return false;
+  }
+  var l = arr1.length;
+  var equalsFn = opt_equalsFn || goog.array.defaultCompareEquality;
+  for (var i = 0; i < l; i++) {
+    if (!equalsFn(arr1[i], arr2[i])) {
+      return false;
+    }
+  }
+  return true;
+};
+
+
+/**
+ * 3-way array compare function.
+ * @param {!IArrayLike<VALUE>} arr1 The first array to
+ *     compare.
+ * @param {!IArrayLike<VALUE>} arr2 The second array to
+ *     compare.
+ * @param {function(VALUE, VALUE): number=} opt_compareFn Optional comparison
+ *     function by which the array is to be ordered. Should take 2 arguments to
+ *     compare, and return a negative number, zero, or a positive number
+ *     depending on whether the first argument is less than, equal to, or
+ *     greater than the second.
+ * @return {number} Negative number, zero, or a positive number depending on
+ *     whether the first argument is less than, equal to, or greater than the
+ *     second.
+ * @template VALUE
+ */
+goog.array.compare3 = function(arr1, arr2, opt_compareFn) {
+  var compare = opt_compareFn || goog.array.defaultCompare;
+  var l = Math.min(arr1.length, arr2.length);
+  for (var i = 0; i < l; i++) {
+    var result = compare(arr1[i], arr2[i]);
+    if (result != 0) {
+      return result;
+    }
+  }
+  return goog.array.defaultCompare(arr1.length, arr2.length);
+};
+
+
+/**
+ * Compares its two arguments for order, using the built in < and >
+ * operators.
+ * @param {VALUE} a The first object to be compared.
+ * @param {VALUE} b The second object to be compared.
+ * @return {number} A negative number, zero, or a positive number as the first
+ *     argument is less than, equal to, or greater than the second,
+ *     respectively.
+ * @template VALUE
+ */
+goog.array.defaultCompare = function(a, b) {
+  return a > b ? 1 : a < b ? -1 : 0;
+};
+
+
+/**
+ * Compares its two arguments for inverse order, using the built in < and >
+ * operators.
+ * @param {VALUE} a The first object to be compared.
+ * @param {VALUE} b The second object to be compared.
+ * @return {number} A negative number, zero, or a positive number as the first
+ *     argument is greater than, equal to, or less than the second,
+ *     respectively.
+ * @template VALUE
+ */
+goog.array.inverseDefaultCompare = function(a, b) {
+  return -goog.array.defaultCompare(a, b);
+};
+
+
+/**
+ * Compares its two arguments for equality, using the built in === operator.
+ * @param {*} a The first object to compare.
+ * @param {*} b The second object to compare.
+ * @return {boolean} True if the two arguments are equal, false otherwise.
+ */
+goog.array.defaultCompareEquality = function(a, b) {
+  return a === b;
+};
+
+
+/**
+ * Inserts a value into a sorted array. The array is not modified if the
+ * value is already present.
+ * @param {IArrayLike<VALUE>} array The array to modify.
+ * @param {VALUE} value The object to insert.
+ * @param {function(VALUE, VALUE): number=} opt_compareFn Optional comparison
+ *     function by which the array is ordered. Should take 2 arguments to
+ *     compare, and return a negative number, zero, or a positive number
+ *     depending on whether the first argument is less than, equal to, or
+ *     greater than the second.
+ * @return {boolean} True if an element was inserted.
+ * @template VALUE
+ */
+goog.array.binaryInsert = function(array, value, opt_compareFn) {
+  var index = goog.array.binarySearch(array, value, opt_compareFn);
+  if (index < 0) {
+    goog.array.insertAt(array, value, -(index + 1));
+    return true;
+  }
+  return false;
+};
+
+
+/**
+ * Removes a value from a sorted array.
+ * @param {!IArrayLike<VALUE>} array The array to modify.
+ * @param {VALUE} value The object to remove.
+ * @param {function(VALUE, VALUE): number=} opt_compareFn Optional comparison
+ *     function by which the array is ordered. Should take 2 arguments to
+ *     compare, and return a negative number, zero, or a positive number
+ *     depending on whether the first argument is less than, equal to, or
+ *     greater than the second.
+ * @return {boolean} True if an element was removed.
+ * @template VALUE
+ */
+goog.array.binaryRemove = function(array, value, opt_compareFn) {
+  var index = goog.array.binarySearch(array, value, opt_compareFn);
+  return (index >= 0) ? goog.array.removeAt(array, index) : false;
+};
+
+
+/**
+ * Splits an array into disjoint buckets according to a splitting function.
+ * @param {Array<T>} array The array.
+ * @param {function(this:S, T, number, !Array<T>):?} sorter Function to call for
+ *     every element.  This takes 3 arguments (the element, the index and the
+ *     array) and must return a valid object key (a string, number, etc), or
+ *     undefined, if that object should not be placed in a bucket.
+ * @param {S=} opt_obj The object to be used as the value of 'this' within
+ *     sorter.
+ * @return {!Object<!Array<T>>} An object, with keys being all of the unique
+ *     return values of sorter, and values being arrays containing the items for
+ *     which the splitter returned that key.
+ * @template T,S
+ */
+goog.array.bucket = function(array, sorter, opt_obj) {
+  var buckets = {};
+
+  for (var i = 0; i < array.length; i++) {
+    var value = array[i];
+    var key = sorter.call(/** @type {?} */ (opt_obj), value, i, array);
+    if (goog.isDef(key)) {
+      // Push the value to the right bucket, creating it if necessary.
+      var bucket = buckets[key] || (buckets[key] = []);
+      bucket.push(value);
+    }
+  }
+
+  return buckets;
+};
+
+
+/**
+ * Creates a new object built from the provided array and the key-generation
+ * function.
+ * @param {IArrayLike<T>} arr Array or array like object over
+ *     which to iterate whose elements will be the values in the new object.
+ * @param {?function(this:S, T, number, ?) : string} keyFunc The function to
+ *     call for every element. This function takes 3 arguments (the element, the
+ *     index and the array) and should return a string that will be used as the
+ *     key for the element in the new object. If the function returns the same
+ *     key for more than one element, the value for that key is
+ *     implementation-defined.
+ * @param {S=} opt_obj The object to be used as the value of 'this'
+ *     within keyFunc.
+ * @return {!Object<T>} The new object.
+ * @template T,S
+ */
+goog.array.toObject = function(arr, keyFunc, opt_obj) {
+  var ret = {};
+  goog.array.forEach(arr, function(element, index) {
+    ret[keyFunc.call(/** @type {?} */ (opt_obj), element, index, arr)] =
+        element;
+  });
+  return ret;
+};
+
+
+/**
+ * Creates a range of numbers in an arithmetic progression.
+ *
+ * Range takes 1, 2, or 3 arguments:
+ * <pre>
+ * range(5) is the same as range(0, 5, 1) and produces [0, 1, 2, 3, 4]
+ * range(2, 5) is the same as range(2, 5, 1) and produces [2, 3, 4]
+ * range(-2, -5, -1) produces [-2, -3, -4]
+ * range(-2, -5, 1) produces [], since stepping by 1 wouldn't ever reach -5.
+ * </pre>
+ *
+ * @param {number} startOrEnd The starting value of the range if an end argument
+ *     is provided. Otherwise, the start value is 0, and this is the end value.
+ * @param {number=} opt_end The optional end value of the range.
+ * @param {number=} opt_step The step size between range values. Defaults to 1
+ *     if opt_step is undefined or 0.
+ * @return {!Array<number>} An array of numbers for the requested range. May be
+ *     an empty array if adding the step would not converge toward the end
+ *     value.
+ */
+goog.array.range = function(startOrEnd, opt_end, opt_step) {
+  var array = [];
+  var start = 0;
+  var end = startOrEnd;
+  var step = opt_step || 1;
+  if (opt_end !== undefined) {
+    start = startOrEnd;
+    end = opt_end;
+  }
+
+  if (step * (end - start) < 0) {
+    // Sign mismatch: start + step will never reach the end value.
+    return [];
+  }
+
+  if (step > 0) {
+    for (var i = start; i < end; i += step) {
+      array.push(i);
+    }
+  } else {
+    for (var i = start; i > end; i += step) {
+      array.push(i);
+    }
+  }
+  return array;
+};
+
+
+/**
+ * Returns an array consisting of the given value repeated N times.
+ *
+ * @param {VALUE} value The value to repeat.
+ * @param {number} n The repeat count.
+ * @return {!Array<VALUE>} An array with the repeated value.
+ * @template VALUE
+ */
+goog.array.repeat = function(value, n) {
+  var array = [];
+  for (var i = 0; i < n; i++) {
+    array[i] = value;
+  }
+  return array;
+};
+
+
+/**
+ * Returns an array consisting of every argument with all arrays
+ * expanded in-place recursively.
+ *
+ * @param {...*} var_args The values to flatten.
+ * @return {!Array<?>} An array containing the flattened values.
+ */
+goog.array.flatten = function(var_args) {
+  var CHUNK_SIZE = 8192;
+
+  var result = [];
+  for (var i = 0; i < arguments.length; i++) {
+    var element = arguments[i];
+    if (goog.isArray(element)) {
+      for (var c = 0; c < element.length; c += CHUNK_SIZE) {
+        var chunk = goog.array.slice(element, c, c + CHUNK_SIZE);
+        var recurseResult = goog.array.flatten.apply(null, chunk);
+        for (var r = 0; r < recurseResult.length; r++) {
+          result.push(recurseResult[r]);
+        }
+      }
+    } else {
+      result.push(element);
+    }
+  }
+  return result;
+};
+
+
+/**
+ * Rotates an array in-place. After calling this method, the element at
+ * index i will be the element previously at index (i - n) %
+ * array.length, for all values of i between 0 and array.length - 1,
+ * inclusive.
+ *
+ * For example, suppose list comprises [t, a, n, k, s]. After invoking
+ * rotate(array, 1) (or rotate(array, -4)), array will comprise [s, t, a, n, k].
+ *
+ * @param {!Array<T>} array The array to rotate.
+ * @param {number} n The amount to rotate.
+ * @return {!Array<T>} The array.
+ * @template T
+ */
+goog.array.rotate = function(array, n) {
+  goog.asserts.assert(array.length != null);
+
+  if (array.length) {
+    n %= array.length;
+    if (n > 0) {
+      Array.prototype.unshift.apply(array, array.splice(-n, n));
+    } else if (n < 0) {
+      Array.prototype.push.apply(array, array.splice(0, -n));
+    }
+  }
+  return array;
+};
+
+
+/**
+ * Moves one item of an array to a new position keeping the order of the rest
+ * of the items. Example use case: keeping a list of JavaScript objects
+ * synchronized with the corresponding list of DOM elements after one of the
+ * elements has been dragged to a new position.
+ * @param {!IArrayLike<?>} arr The array to modify.
+ * @param {number} fromIndex Index of the item to move between 0 and
+ *     {@code arr.length - 1}.
+ * @param {number} toIndex Target index between 0 and {@code arr.length - 1}.
+ */
+goog.array.moveItem = function(arr, fromIndex, toIndex) {
+  goog.asserts.assert(fromIndex >= 0 && fromIndex < arr.length);
+  goog.asserts.assert(toIndex >= 0 && toIndex < arr.length);
+  // Remove 1 item at fromIndex.
+  var removedItems = Array.prototype.splice.call(arr, fromIndex, 1);
+  // Insert the removed item at toIndex.
+  Array.prototype.splice.call(arr, toIndex, 0, removedItems[0]);
+  // We don't use goog.array.insertAt and goog.array.removeAt, because they're
+  // significantly slower than splice.
+};
+
+
+/**
+ * Creates a new array for which the element at position i is an array of the
+ * ith element of the provided arrays.  The returned array will only be as long
+ * as the shortest array provided; additional values are ignored.  For example,
+ * the result of zipping [1, 2] and [3, 4, 5] is [[1,3], [2, 4]].
+ *
+ * This is similar to the zip() function in Python.  See {@link
+ * http://docs.python.org/library/functions.html#zip}
+ *
+ * @param {...!IArrayLike<?>} var_args Arrays to be combined.
+ * @return {!Array<!Array<?>>} A new array of arrays created from
+ *     provided arrays.
+ */
+goog.array.zip = function(var_args) {
+  if (!arguments.length) {
+    return [];
+  }
+  var result = [];
+  var minLen = arguments[0].length;
+  for (var i = 1; i < arguments.length; i++) {
+    if (arguments[i].length < minLen) {
+      minLen = arguments[i].length;
+    }
+  }
+  for (var i = 0; i < minLen; i++) {
+    var value = [];
+    for (var j = 0; j < arguments.length; j++) {
+      value.push(arguments[j][i]);
+    }
+    result.push(value);
+  }
+  return result;
+};
+
+
+/**
+ * Shuffles the values in the specified array using the Fisher-Yates in-place
+ * shuffle (also known as the Knuth Shuffle). By default, calls Math.random()
+ * and so resets the state of that random number generator. Similarly, may reset
+ * the state of the any other specified random number generator.
+ *
+ * Runtime: O(n)
+ *
+ * @param {!Array<?>} arr The array to be shuffled.
+ * @param {function():number=} opt_randFn Optional random function to use for
+ *     shuffling.
+ *     Takes no arguments, and returns a random number on the interval [0, 1).
+ *     Defaults to Math.random() using JavaScript's built-in Math library.
+ */
+goog.array.shuffle = function(arr, opt_randFn) {
+  var randFn = opt_randFn || Math.random;
+
+  for (var i = arr.length - 1; i > 0; i--) {
+    // Choose a random array index in [0, i] (inclusive with i).
+    var j = Math.floor(randFn() * (i + 1));
+
+    var tmp = arr[i];
+    arr[i] = arr[j];
+    arr[j] = tmp;
+  }
+};
+
+
+/**
+ * Returns a new array of elements from arr, based on the indexes of elements
+ * provided by index_arr. For example, the result of index copying
+ * ['a', 'b', 'c'] with index_arr [1,0,0,2] is ['b', 'a', 'a', 'c'].
+ *
+ * @param {!Array<T>} arr The array to get a indexed copy from.
+ * @param {!Array<number>} index_arr An array of indexes to get from arr.
+ * @return {!Array<T>} A new array of elements from arr in index_arr order.
+ * @template T
+ */
+goog.array.copyByIndex = function(arr, index_arr) {
+  var result = [];
+  goog.array.forEach(index_arr, function(index) { result.push(arr[index]); });
+  return result;
+};
+
+
+/**
+ * Maps each element of the input array into zero or more elements of the output
+ * array.
+ *
+ * @param {!IArrayLike<VALUE>|string} arr Array or array like object
+ *     over which to iterate.
+ * @param {function(this:THIS, VALUE, number, ?): !Array<RESULT>} f The function
+ *     to call for every element. This function takes 3 arguments (the element,
+ *     the index and the array) and should return an array. The result will be
+ *     used to extend a new array.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within f.
+ * @return {!Array<RESULT>} a new array with the concatenation of all arrays
+ *     returned from f.
+ * @template THIS, VALUE, RESULT
+ */
+goog.array.concatMap = function(arr, f, opt_obj) {
+  return goog.array.concat.apply([], goog.array.map(arr, f, opt_obj));
+};
diff --git a/third_party/ink/closure/asserts/asserts.js b/third_party/ink/closure/asserts/asserts.js
new file mode 100644
index 0000000..89cad0a
--- /dev/null
+++ b/third_party/ink/closure/asserts/asserts.js
@@ -0,0 +1,391 @@
+// Copyright 2008 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 Utilities to check the preconditions, postconditions and
+ * invariants runtime.
+ *
+ * Methods in this package should be given special treatment by the compiler
+ * for type-inference. For example, <code>goog.asserts.assert(foo)</code>
+ * will restrict <code>foo</code> to a truthy value.
+ *
+ * The compiler has an option to disable asserts. So code like:
+ * <code>
+ * var x = goog.asserts.assert(foo()); goog.asserts.assert(bar());
+ * </code>
+ * will be transformed into:
+ * <code>
+ * var x = foo();
+ * </code>
+ * The compiler will leave in foo() (because its return value is used),
+ * but it will remove bar() because it assumes it does not have side-effects.
+ *
+ * @author pallosp@google.com (Peter Pallos)
+ * @author agrieve@google.com (Andrew Grieve)
+ */
+
+goog.provide('goog.asserts');
+goog.provide('goog.asserts.AssertionError');
+
+goog.require('goog.debug.Error');
+goog.require('goog.dom.NodeType');
+goog.require('goog.string');
+
+
+/**
+ * @define {boolean} Whether to strip out asserts or to leave them in.
+ */
+goog.define('goog.asserts.ENABLE_ASSERTS', goog.DEBUG);
+
+
+
+/**
+ * Error object for failed assertions.
+ * @param {string} messagePattern The pattern that was used to form message.
+ * @param {!Array<*>} messageArgs The items to substitute into the pattern.
+ * @constructor
+ * @extends {goog.debug.Error}
+ * @final
+ */
+goog.asserts.AssertionError = function(messagePattern, messageArgs) {
+  messageArgs.unshift(messagePattern);
+  goog.debug.Error.call(this, goog.string.subs.apply(null, messageArgs));
+  // Remove the messagePattern afterwards to avoid permanently modifying the
+  // passed in array.
+  messageArgs.shift();
+
+  /**
+   * The message pattern used to format the error message. Error handlers can
+   * use this to uniquely identify the assertion.
+   * @type {string}
+   */
+  this.messagePattern = messagePattern;
+};
+goog.inherits(goog.asserts.AssertionError, goog.debug.Error);
+
+
+/** @override */
+goog.asserts.AssertionError.prototype.name = 'AssertionError';
+
+
+/**
+ * The default error handler.
+ * @param {!goog.asserts.AssertionError} e The exception to be handled.
+ */
+goog.asserts.DEFAULT_ERROR_HANDLER = function(e) {
+  throw e;
+};
+
+
+/**
+ * The handler responsible for throwing or logging assertion errors.
+ * @private {function(!goog.asserts.AssertionError)}
+ */
+goog.asserts.errorHandler_ = goog.asserts.DEFAULT_ERROR_HANDLER;
+
+
+/**
+ * Throws an exception with the given message and "Assertion failed" prefixed
+ * onto it.
+ * @param {string} defaultMessage The message to use if givenMessage is empty.
+ * @param {Array<*>} defaultArgs The substitution arguments for defaultMessage.
+ * @param {string|undefined} givenMessage Message supplied by the caller.
+ * @param {Array<*>} givenArgs The substitution arguments for givenMessage.
+ * @throws {goog.asserts.AssertionError} When the value is not a number.
+ * @private
+ */
+goog.asserts.doAssertFailure_ = function(
+    defaultMessage, defaultArgs, givenMessage, givenArgs) {
+  var message = 'Assertion failed';
+  if (givenMessage) {
+    message += ': ' + givenMessage;
+    var args = givenArgs;
+  } else if (defaultMessage) {
+    message += ': ' + defaultMessage;
+    args = defaultArgs;
+  }
+  // The '' + works around an Opera 10 bug in the unit tests. Without it,
+  // a stack trace is added to var message above. With this, a stack trace is
+  // not added until this line (it causes the extra garbage to be added after
+  // the assertion message instead of in the middle of it).
+  var e = new goog.asserts.AssertionError('' + message, args || []);
+  goog.asserts.errorHandler_(e);
+};
+
+
+/**
+ * Sets a custom error handler that can be used to customize the behavior of
+ * assertion failures, for example by turning all assertion failures into log
+ * messages.
+ * @param {function(!goog.asserts.AssertionError)} errorHandler
+ */
+goog.asserts.setErrorHandler = function(errorHandler) {
+  if (goog.asserts.ENABLE_ASSERTS) {
+    goog.asserts.errorHandler_ = errorHandler;
+  }
+};
+
+
+/**
+ * Checks if the condition evaluates to true if goog.asserts.ENABLE_ASSERTS is
+ * true.
+ * @template T
+ * @param {T} condition The condition to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {T} The value of the condition.
+ * @throws {goog.asserts.AssertionError} When the condition evaluates to false.
+ */
+goog.asserts.assert = function(condition, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !condition) {
+    goog.asserts.doAssertFailure_(
+        '', null, opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return condition;
+};
+
+
+/**
+ * Fails if goog.asserts.ENABLE_ASSERTS is true. This function is useful in case
+ * when we want to add a check in the unreachable area like switch-case
+ * statement:
+ *
+ * <pre>
+ *  switch(type) {
+ *    case FOO: doSomething(); break;
+ *    case BAR: doSomethingElse(); break;
+ *    default: goog.asserts.fail('Unrecognized type: ' + type);
+ *      // We have only 2 types - "default:" section is unreachable code.
+ *  }
+ * </pre>
+ *
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @throws {goog.asserts.AssertionError} Failure.
+ */
+goog.asserts.fail = function(opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS) {
+    goog.asserts.errorHandler_(
+        new goog.asserts.AssertionError(
+            'Failure' + (opt_message ? ': ' + opt_message : ''),
+            Array.prototype.slice.call(arguments, 1)));
+  }
+};
+
+
+/**
+ * Checks if the value is a number if goog.asserts.ENABLE_ASSERTS is true.
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {number} The value, guaranteed to be a number when asserts enabled.
+ * @throws {goog.asserts.AssertionError} When the value is not a number.
+ */
+goog.asserts.assertNumber = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !goog.isNumber(value)) {
+    goog.asserts.doAssertFailure_(
+        'Expected number but got %s: %s.', [goog.typeOf(value), value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {number} */ (value);
+};
+
+
+/**
+ * Checks if the value is a string if goog.asserts.ENABLE_ASSERTS is true.
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {string} The value, guaranteed to be a string when asserts enabled.
+ * @throws {goog.asserts.AssertionError} When the value is not a string.
+ */
+goog.asserts.assertString = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !goog.isString(value)) {
+    goog.asserts.doAssertFailure_(
+        'Expected string but got %s: %s.', [goog.typeOf(value), value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {string} */ (value);
+};
+
+
+/**
+ * Checks if the value is a function if goog.asserts.ENABLE_ASSERTS is true.
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {!Function} The value, guaranteed to be a function when asserts
+ *     enabled.
+ * @throws {goog.asserts.AssertionError} When the value is not a function.
+ */
+goog.asserts.assertFunction = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !goog.isFunction(value)) {
+    goog.asserts.doAssertFailure_(
+        'Expected function but got %s: %s.', [goog.typeOf(value), value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {!Function} */ (value);
+};
+
+
+/**
+ * Checks if the value is an Object if goog.asserts.ENABLE_ASSERTS is true.
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {!Object} The value, guaranteed to be a non-null object.
+ * @throws {goog.asserts.AssertionError} When the value is not an object.
+ */
+goog.asserts.assertObject = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !goog.isObject(value)) {
+    goog.asserts.doAssertFailure_(
+        'Expected object but got %s: %s.', [goog.typeOf(value), value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {!Object} */ (value);
+};
+
+
+/**
+ * Checks if the value is an Array if goog.asserts.ENABLE_ASSERTS is true.
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {!Array<?>} The value, guaranteed to be a non-null array.
+ * @throws {goog.asserts.AssertionError} When the value is not an array.
+ */
+goog.asserts.assertArray = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !goog.isArray(value)) {
+    goog.asserts.doAssertFailure_(
+        'Expected array but got %s: %s.', [goog.typeOf(value), value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {!Array<?>} */ (value);
+};
+
+
+/**
+ * Checks if the value is a boolean if goog.asserts.ENABLE_ASSERTS is true.
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {boolean} The value, guaranteed to be a boolean when asserts are
+ *     enabled.
+ * @throws {goog.asserts.AssertionError} When the value is not a boolean.
+ */
+goog.asserts.assertBoolean = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !goog.isBoolean(value)) {
+    goog.asserts.doAssertFailure_(
+        'Expected boolean but got %s: %s.', [goog.typeOf(value), value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {boolean} */ (value);
+};
+
+
+/**
+ * Checks if the value is a DOM Element if goog.asserts.ENABLE_ASSERTS is true.
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {!Element} The value, likely to be a DOM Element when asserts are
+ *     enabled.
+ * @throws {goog.asserts.AssertionError} When the value is not an Element.
+ */
+goog.asserts.assertElement = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS &&
+      (!goog.isObject(value) || value.nodeType != goog.dom.NodeType.ELEMENT)) {
+    goog.asserts.doAssertFailure_(
+        'Expected Element but got %s: %s.', [goog.typeOf(value), value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {!Element} */ (value);
+};
+
+
+/**
+ * Checks if the value is an instance of the user-defined type if
+ * goog.asserts.ENABLE_ASSERTS is true.
+ *
+ * The compiler may tighten the type returned by this function.
+ *
+ * @param {?} value The value to check.
+ * @param {function(new: T, ...)} type A user-defined constructor.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @throws {goog.asserts.AssertionError} When the value is not an instance of
+ *     type.
+ * @return {T}
+ * @template T
+ */
+goog.asserts.assertInstanceof = function(value, type, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !(value instanceof type)) {
+    goog.asserts.doAssertFailure_(
+        'Expected instanceof %s but got %s.',
+        [goog.asserts.getType_(type), goog.asserts.getType_(value)],
+        opt_message, Array.prototype.slice.call(arguments, 3));
+  }
+  return value;
+};
+
+
+/**
+ * Checks whether the value is a finite number, if goog.asserts.ENABLE_ASSERTS
+ * is true.
+ *
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @throws {goog.asserts.AssertionError} When the value is not a number, or is
+ *     a non-finite number such as NaN, Infinity or -Infinity.
+ * @return {number} The value initially passed in.
+ */
+goog.asserts.assertFinite = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS &&
+      (typeof value != 'number' || !isFinite(value))) {
+    goog.asserts.doAssertFailure_(
+        'Expected %s to be a finite number but it is not.', [value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {number} */ (value);
+};
+
+/**
+ * Checks that no enumerable keys are present in Object.prototype. Such keys
+ * would break most code that use {@code for (var ... in ...)} loops.
+ */
+goog.asserts.assertObjectPrototypeIsIntact = function() {
+  for (var key in Object.prototype) {
+    goog.asserts.fail(key + ' should not be enumerable in Object.prototype.');
+  }
+};
+
+
+/**
+ * Returns the type of a value. If a constructor is passed, and a suitable
+ * string cannot be found, 'unknown type name' will be returned.
+ * @param {*} value A constructor, object, or primitive.
+ * @return {string} The best display name for the value, or 'unknown type name'.
+ * @private
+ */
+goog.asserts.getType_ = function(value) {
+  if (value instanceof Function) {
+    return value.displayName || value.name || 'unknown type name';
+  } else if (value instanceof Object) {
+    return value.constructor.displayName || value.constructor.name ||
+        Object.prototype.toString.call(value);
+  } else {
+    return value === null ? 'null' : typeof value;
+  }
+};
diff --git a/third_party/ink/closure/base.js b/third_party/ink/closure/base.js
new file mode 100644
index 0000000..4d46cd7
--- /dev/null
+++ b/third_party/ink/closure/base.js
@@ -0,0 +1,2962 @@
+// Copyright 2006 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 Bootstrap for the Google JS Library (Closure).
+ *
+ * In uncompiled mode base.js will attempt to load Closure's deps file, unless
+ * the global <code>CLOSURE_NO_DEPS</code> is set to true.  This allows projects
+ * to include their own deps file(s) from different locations.
+ *
+ * Avoid including base.js more than once. This is strictly discouraged and not
+ * supported. goog.require(...) won't work properly in that case.
+ *
+ * @provideGoog
+ */
+
+
+/**
+ * @define {boolean} Overridden to true by the compiler.
+ */
+var COMPILED = false;
+
+
+/**
+ * Base namespace for the Closure library.  Checks to see goog is already
+ * defined in the current scope before assigning to prevent clobbering if
+ * base.js is loaded more than once.
+ *
+ * @const
+ */
+var goog = goog || {};
+
+
+/**
+ * Reference to the global context.  In most cases this will be 'window'.
+ */
+goog.global = this;
+
+
+/**
+ * A hook for overriding the define values in uncompiled mode.
+ *
+ * In uncompiled mode, {@code CLOSURE_UNCOMPILED_DEFINES} may be defined before
+ * loading base.js.  If a key is defined in {@code CLOSURE_UNCOMPILED_DEFINES},
+ * {@code goog.define} will use the value instead of the default value.  This
+ * allows flags to be overwritten without compilation (this is normally
+ * accomplished with the compiler's "define" flag).
+ *
+ * Example:
+ * <pre>
+ *   var CLOSURE_UNCOMPILED_DEFINES = {'goog.DEBUG': false};
+ * </pre>
+ *
+ * @type {Object<string, (string|number|boolean)>|undefined}
+ */
+goog.global.CLOSURE_UNCOMPILED_DEFINES;
+
+
+/**
+ * A hook for overriding the define values in uncompiled or compiled mode,
+ * like CLOSURE_UNCOMPILED_DEFINES but effective in compiled code.  In
+ * uncompiled code CLOSURE_UNCOMPILED_DEFINES takes precedence.
+ *
+ * Also unlike CLOSURE_UNCOMPILED_DEFINES the values must be number, boolean or
+ * string literals or the compiler will emit an error.
+ *
+ * While any @define value may be set, only those set with goog.define will be
+ * effective for uncompiled code.
+ *
+ * Example:
+ * <pre>
+ *   var CLOSURE_DEFINES = {'goog.DEBUG': false} ;
+ * </pre>
+ *
+ * @type {Object<string, (string|number|boolean)>|undefined}
+ */
+goog.global.CLOSURE_DEFINES;
+
+
+/**
+ * Returns true if the specified value is not undefined.
+ *
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is defined.
+ */
+goog.isDef = function(val) {
+  // void 0 always evaluates to undefined and hence we do not need to depend on
+  // the definition of the global variable named 'undefined'.
+  return val !== void 0;
+};
+
+/**
+ * Returns true if the specified value is a string.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is a string.
+ */
+goog.isString = function(val) {
+  return typeof val == 'string';
+};
+
+
+/**
+ * Returns true if the specified value is a boolean.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is boolean.
+ */
+goog.isBoolean = function(val) {
+  return typeof val == 'boolean';
+};
+
+
+/**
+ * Returns true if the specified value is a number.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is a number.
+ */
+goog.isNumber = function(val) {
+  return typeof val == 'number';
+};
+
+
+/**
+ * Builds an object structure for the provided namespace path, ensuring that
+ * names that already exist are not overwritten. For example:
+ * "a.b.c" -> a = {};a.b={};a.b.c={};
+ * Used by goog.provide and goog.exportSymbol.
+ * @param {string} name name of the object that this file defines.
+ * @param {*=} opt_object the object to expose at the end of the path.
+ * @param {Object=} opt_objectToExportTo The object to add the path to; default
+ *     is `goog.global`.
+ * @private
+ */
+goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) {
+  var parts = name.split('.');
+  var cur = opt_objectToExportTo || goog.global;
+
+  // Internet Explorer exhibits strange behavior when throwing errors from
+  // methods externed in this manner.  See the testExportSymbolExceptions in
+  // base_test.html for an example.
+  if (!(parts[0] in cur) && cur.execScript) {
+    cur.execScript('var ' + parts[0]);
+  }
+
+  for (var part; parts.length && (part = parts.shift());) {
+    if (!parts.length && goog.isDef(opt_object)) {
+      // last part and we have an object; use it
+      cur[part] = opt_object;
+    } else if (cur[part] && cur[part] !== Object.prototype[part]) {
+      cur = cur[part];
+    } else {
+      cur = cur[part] = {};
+    }
+  }
+};
+
+
+/**
+ * Defines a named value. In uncompiled mode, the value is retrieved from
+ * CLOSURE_DEFINES or CLOSURE_UNCOMPILED_DEFINES if the object is defined and
+ * has the property specified, and otherwise used the defined defaultValue.
+ * When compiled the default can be overridden using the compiler
+ * options or the value set in the CLOSURE_DEFINES object.
+ *
+ * @param {string} name The distinguished name to provide.
+ * @param {string|number|boolean} defaultValue
+ */
+goog.define = function(name, defaultValue) {
+  var value = defaultValue;
+  if (!COMPILED) {
+    if (goog.global.CLOSURE_UNCOMPILED_DEFINES &&
+        // Anti DOM-clobbering runtime check (b/37736576).
+        /** @type {?} */ (goog.global.CLOSURE_UNCOMPILED_DEFINES).nodeType ===
+            undefined &&
+        Object.prototype.hasOwnProperty.call(
+            goog.global.CLOSURE_UNCOMPILED_DEFINES, name)) {
+      value = goog.global.CLOSURE_UNCOMPILED_DEFINES[name];
+    } else if (
+        goog.global.CLOSURE_DEFINES &&
+        // Anti DOM-clobbering runtime check (b/37736576).
+        /** @type {?} */ (goog.global.CLOSURE_DEFINES).nodeType === undefined &&
+        Object.prototype.hasOwnProperty.call(
+            goog.global.CLOSURE_DEFINES, name)) {
+      value = goog.global.CLOSURE_DEFINES[name];
+    }
+  }
+  goog.exportPath_(name, value);
+};
+
+
+/**
+ * @define {boolean} DEBUG is provided as a convenience so that debugging code
+ * that should not be included in a production. It can be easily stripped
+ * by specifying --define goog.DEBUG=false to the Closure Compiler aka
+ * JSCompiler. For example, most toString() methods should be declared inside an
+ * "if (goog.DEBUG)" conditional because they are generally used for debugging
+ * purposes and it is difficult for the JSCompiler to statically determine
+ * whether they are used.
+ */
+goog.define('goog.DEBUG', true);
+
+
+/**
+ * @define {string} LOCALE defines the locale being used for compilation. It is
+ * used to select locale specific data to be compiled in js binary. BUILD rule
+ * can specify this value by "--define goog.LOCALE=<locale_name>" as a compiler
+ * option.
+ *
+ * Take into account that the locale code format is important. You should use
+ * the canonical Unicode format with hyphen as a delimiter. Language must be
+ * lowercase, Language Script - Capitalized, Region - UPPERCASE.
+ * There are few examples: pt-BR, en, en-US, sr-Latin-BO, zh-Hans-CN.
+ *
+ * See more info about locale codes here:
+ * http://www.unicode.org/reports/tr35/#Unicode_Language_and_Locale_Identifiers
+ *
+ * For language codes you should use values defined by ISO 693-1. See it here
+ * http://www.w3.org/WAI/ER/IG/ert/iso639.htm. There is only one exception from
+ * this rule: the Hebrew language. For legacy reasons the old code (iw) should
+ * be used instead of the new code (he).
+ *
+ * MOE:begin_intracomment_strip
+ * See http://g3doc/i18n/identifiers/g3doc/synonyms.
+ * MOE:end_intracomment_strip
+ */
+goog.define('goog.LOCALE', 'en');  // default to en
+
+
+/**
+ * @define {boolean} Whether this code is running on trusted sites.
+ *
+ * On untrusted sites, several native functions can be defined or overridden by
+ * external libraries like Prototype, Datejs, and JQuery and setting this flag
+ * to false forces closure to use its own implementations when possible.
+ *
+ * If your JavaScript can be loaded by a third party site and you are wary about
+ * relying on non-standard implementations, specify
+ * "--define goog.TRUSTED_SITE=false" to the compiler.
+ */
+goog.define('goog.TRUSTED_SITE', true);
+
+
+/**
+ * @define {boolean} Whether a project is expected to be running in strict mode.
+ *
+ * This define can be used to trigger alternate implementations compatible with
+ * running in EcmaScript Strict mode or warn about unavailable functionality.
+ * @see https://goo.gl/PudQ4y
+ *
+ */
+goog.define('goog.STRICT_MODE_COMPATIBLE', false);
+
+
+/**
+ * @define {boolean} Whether code that calls {@link goog.setTestOnly} should
+ *     be disallowed in the compilation unit.
+ */
+goog.define('goog.DISALLOW_TEST_ONLY_CODE', COMPILED && !goog.DEBUG);
+
+
+/**
+ * @define {boolean} Whether to use a Chrome app CSP-compliant method for
+ *     loading scripts via goog.require. @see appendScriptSrcNode_.
+ */
+goog.define('goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING', false);
+
+
+/**
+ * Defines a namespace in Closure.
+ *
+ * A namespace may only be defined once in a codebase. It may be defined using
+ * goog.provide() or goog.module().
+ *
+ * The presence of one or more goog.provide() calls in a file indicates
+ * that the file defines the given objects/namespaces.
+ * Provided symbols must not be null or undefined.
+ *
+ * In addition, goog.provide() creates the object stubs for a namespace
+ * (for example, goog.provide("goog.foo.bar") will create the object
+ * goog.foo.bar if it does not already exist).
+ *
+ * Build tools also scan for provide/require/module statements
+ * to discern dependencies, build dependency files (see deps.js), etc.
+ *
+ * @see goog.require
+ * @see goog.module
+ * @param {string} name Namespace provided by this file in the form
+ *     "goog.package.part".
+ */
+goog.provide = function(name) {
+  if (goog.isInModuleLoader_()) {
+    throw new Error('goog.provide can not be used within a goog.module.');
+  }
+  if (!COMPILED) {
+    // Ensure that the same namespace isn't provided twice.
+    // A goog.module/goog.provide maps a goog.require to a specific file
+    if (goog.isProvided_(name)) {
+      throw new Error('Namespace "' + name + '" already declared.');
+    }
+  }
+
+  goog.constructNamespace_(name);
+};
+
+
+/**
+ * @param {string} name Namespace provided by this file in the form
+ *     "goog.package.part".
+ * @param {Object=} opt_obj The object to embed in the namespace.
+ * @private
+ */
+goog.constructNamespace_ = function(name, opt_obj) {
+  if (!COMPILED) {
+    delete goog.implicitNamespaces_[name];
+
+    var namespace = name;
+    while ((namespace = namespace.substring(0, namespace.lastIndexOf('.')))) {
+      if (goog.getObjectByName(namespace)) {
+        break;
+      }
+      goog.implicitNamespaces_[namespace] = true;
+    }
+  }
+
+  goog.exportPath_(name, opt_obj);
+};
+
+
+/**
+ * Module identifier validation regexp.
+ * Note: This is a conservative check, it is very possible to be more lenient,
+ *   the primary exclusion here is "/" and "\" and a leading ".", these
+ *   restrictions are intended to leave the door open for using goog.require
+ *   with relative file paths rather than module identifiers.
+ * @private
+ */
+goog.VALID_MODULE_RE_ = /^[a-zA-Z_$][a-zA-Z0-9._$]*$/;
+
+
+/**
+ * Defines a module in Closure.
+ *
+ * Marks that this file must be loaded as a module and claims the namespace.
+ *
+ * A namespace may only be defined once in a codebase. It may be defined using
+ * goog.provide() or goog.module().
+ *
+ * goog.module() has three requirements:
+ * - goog.module may not be used in the same file as goog.provide.
+ * - goog.module must be the first statement in the file.
+ * - only one goog.module is allowed per file.
+ *
+ * When a goog.module annotated file is loaded, it is enclosed in
+ * a strict function closure. This means that:
+ * - any variables declared in a goog.module file are private to the file
+ * (not global), though the compiler is expected to inline the module.
+ * - The code must obey all the rules of "strict" JavaScript.
+ * - the file will be marked as "use strict"
+ *
+ * NOTE: unlike goog.provide, goog.module does not declare any symbols by
+ * itself. If declared symbols are desired, use
+ * goog.module.declareLegacyNamespace().
+ *
+ * MOE:begin_intracomment_strip
+ * See the goog.module announcement at http://go/goog.module-announce
+ * MOE:end_intracomment_strip
+ *
+ * See the public goog.module proposal: http://goo.gl/Va1hin
+ *
+ * @param {string} name Namespace provided by this file in the form
+ *     "goog.package.part", is expected but not required.
+ * @return {void}
+ */
+goog.module = function(name) {
+  if (!goog.isString(name) || !name ||
+      name.search(goog.VALID_MODULE_RE_) == -1) {
+    throw new Error('Invalid module identifier');
+  }
+  if (!goog.isInModuleLoader_()) {
+    throw new Error(
+        'Module ' + name + ' has been loaded incorrectly. Note, ' +
+        'modules cannot be loaded as normal scripts. They require some kind of ' +
+        'pre-processing step. You\'re likely trying to load a module via a ' +
+        'script tag or as a part of a concatenated bundle without rewriting the ' +
+        'module. For more info see: ' +
+        'https://github.com/google/closure-library/wiki/goog.module:-an-ES6-module-like-alternative-to-goog.provide.');
+  }
+  if (goog.moduleLoaderState_.moduleName) {
+    throw new Error('goog.module may only be called once per module.');
+  }
+
+  // Store the module name for the loader.
+  goog.moduleLoaderState_.moduleName = name;
+  if (!COMPILED) {
+    // Ensure that the same namespace isn't provided twice.
+    // A goog.module/goog.provide maps a goog.require to a specific file
+    if (goog.isProvided_(name)) {
+      throw new Error('Namespace "' + name + '" already declared.');
+    }
+    delete goog.implicitNamespaces_[name];
+  }
+};
+
+
+/**
+ * @param {string} name The module identifier.
+ * @return {?} The module exports for an already loaded module or null.
+ *
+ * Note: This is not an alternative to goog.require, it does not
+ * indicate a hard dependency, instead it is used to indicate
+ * an optional dependency or to access the exports of a module
+ * that has already been loaded.
+ * @suppress {missingProvide}
+ */
+goog.module.get = function(name) {
+  return goog.module.getInternal_(name);
+};
+
+
+/**
+ * @param {string} name The module identifier.
+ * @return {?} The module exports for an already loaded module or null.
+ * @private
+ */
+goog.module.getInternal_ = function(name) {
+  if (!COMPILED) {
+    if (name in goog.loadedModules_) {
+      return goog.loadedModules_[name];
+    } else if (!goog.implicitNamespaces_[name]) {
+      var ns = goog.getObjectByName(name);
+      return ns != null ? ns : null;
+    }
+  }
+  return null;
+};
+
+
+/**
+ * @private {?{moduleName: (string|undefined), declareLegacyNamespace:boolean}}
+ */
+goog.moduleLoaderState_ = null;
+
+
+/**
+ * @private
+ * @return {boolean} Whether a goog.module is currently being initialized.
+ */
+goog.isInModuleLoader_ = function() {
+  return goog.moduleLoaderState_ != null;
+};
+
+
+/**
+ * Provide the module's exports as a globally accessible object under the
+ * module's declared name.  This is intended to ease migration to goog.module
+ * for files that have existing usages.
+ * @suppress {missingProvide}
+ */
+goog.module.declareLegacyNamespace = function() {
+  if (!COMPILED && !goog.isInModuleLoader_()) {
+    throw new Error(
+        'goog.module.declareLegacyNamespace must be called from ' +
+        'within a goog.module');
+  }
+  if (!COMPILED && !goog.moduleLoaderState_.moduleName) {
+    throw new Error(
+        'goog.module must be called prior to ' +
+        'goog.module.declareLegacyNamespace.');
+  }
+  goog.moduleLoaderState_.declareLegacyNamespace = true;
+};
+
+
+/**
+ * Marks that the current file should only be used for testing, and never for
+ * live code in production.
+ *
+ * In the case of unit tests, the message may optionally be an exact namespace
+ * for the test (e.g. 'goog.stringTest'). The linter will then ignore the extra
+ * provide (if not explicitly defined in the code).
+ *
+ * @param {string=} opt_message Optional message to add to the error that's
+ *     raised when used in production code.
+ */
+goog.setTestOnly = function(opt_message) {
+  if (goog.DISALLOW_TEST_ONLY_CODE) {
+    opt_message = opt_message || '';
+    throw new Error(
+        'Importing test-only code into non-debug environment' +
+        (opt_message ? ': ' + opt_message : '.'));
+  }
+};
+
+
+/**
+ * Forward declares a symbol. This is an indication to the compiler that the
+ * symbol may be used in the source yet is not required and may not be provided
+ * in compilation.
+ *
+ * The most common usage of forward declaration is code that takes a type as a
+ * function parameter but does not need to require it. By forward declaring
+ * instead of requiring, no hard dependency is made, and (if not required
+ * elsewhere) the namespace may never be required and thus, not be pulled
+ * into the JavaScript binary. If it is required elsewhere, it will be type
+ * checked as normal.
+ *
+ * Before using goog.forwardDeclare, please read the documentation at
+ * https://github.com/google/closure-compiler/wiki/Bad-Type-Annotation to
+ * understand the options and tradeoffs when working with forward declarations.
+ *
+ * @param {string} name The namespace to forward declare in the form of
+ *     "goog.package.part".
+ */
+goog.forwardDeclare = function(name) {};
+
+
+/**
+ * Forward declare type information. Used to assign types to goog.global
+ * referenced object that would otherwise result in unknown type references
+ * and thus block property disambiguation.
+ */
+goog.forwardDeclare('Document');
+goog.forwardDeclare('HTMLScriptElement');
+goog.forwardDeclare('XMLHttpRequest');
+
+
+if (!COMPILED) {
+  /**
+   * Check if the given name has been goog.provided. This will return false for
+   * names that are available only as implicit namespaces.
+   * @param {string} name name of the object to look for.
+   * @return {boolean} Whether the name has been provided.
+   * @private
+   */
+  goog.isProvided_ = function(name) {
+    return (name in goog.loadedModules_) ||
+        (!goog.implicitNamespaces_[name] &&
+         goog.isDefAndNotNull(goog.getObjectByName(name)));
+  };
+
+  /**
+   * Namespaces implicitly defined by goog.provide. For example,
+   * goog.provide('goog.events.Event') implicitly declares that 'goog' and
+   * 'goog.events' must be namespaces.
+   *
+   * @type {!Object<string, (boolean|undefined)>}
+   * @private
+   */
+  goog.implicitNamespaces_ = {'goog.module': true};
+
+  // NOTE: We add goog.module as an implicit namespace as goog.module is defined
+  // here and because the existing module package has not been moved yet out of
+  // the goog.module namespace. This satisifies both the debug loader and
+  // ahead-of-time dependency management.
+}
+
+
+/**
+ * Returns an object based on its fully qualified external name.  The object
+ * is not found if null or undefined.  If you are using a compilation pass that
+ * renames property names beware that using this function will not find renamed
+ * properties.
+ *
+ * @param {string} name The fully qualified name.
+ * @param {Object=} opt_obj The object within which to look; default is
+ *     |goog.global|.
+ * @return {?} The value (object or primitive) or, if not found, null.
+ */
+goog.getObjectByName = function(name, opt_obj) {
+  var parts = name.split('.');
+  var cur = opt_obj || goog.global;
+  for (var i = 0; i < parts.length; i++) {
+    cur = cur[parts[i]];
+    if (!goog.isDefAndNotNull(cur)) {
+      return null;
+    }
+  }
+  return cur;
+};
+
+
+/**
+ * Globalizes a whole namespace, such as goog or goog.lang.
+ *
+ * @param {!Object} obj The namespace to globalize.
+ * @param {Object=} opt_global The object to add the properties to.
+ * @deprecated Properties may be explicitly exported to the global scope, but
+ *     this should no longer be done in bulk.
+ */
+goog.globalize = function(obj, opt_global) {
+  var global = opt_global || goog.global;
+  for (var x in obj) {
+    global[x] = obj[x];
+  }
+};
+
+
+/**
+ * Adds a dependency from a file to the files it requires.
+ * @param {string} relPath The path to the js file.
+ * @param {!Array<string>} provides An array of strings with
+ *     the names of the objects this file provides.
+ * @param {!Array<string>} requires An array of strings with
+ *     the names of the objects this file requires.
+ * @param {boolean|!Object<string>=} opt_loadFlags Parameters indicating
+ *     how the file must be loaded.  The boolean 'true' is equivalent
+ *     to {'module': 'goog'} for backwards-compatibility.  Valid properties
+ *     and values include {'module': 'goog'} and {'lang': 'es6'}.
+ */
+goog.addDependency = function(relPath, provides, requires, opt_loadFlags) {
+  if (goog.DEPENDENCIES_ENABLED) {
+    var provide, require;
+    var path = relPath.replace(/\\/g, '/');
+    var deps = goog.dependencies_;
+    if (!opt_loadFlags || typeof opt_loadFlags === 'boolean') {
+      opt_loadFlags = opt_loadFlags ? {'module': 'goog'} : {};
+    }
+    for (var i = 0; provide = provides[i]; i++) {
+      deps.nameToPath[provide] = path;
+      deps.loadFlags[path] = opt_loadFlags;
+    }
+    for (var j = 0; require = requires[j]; j++) {
+      if (!(path in deps.requires)) {
+        deps.requires[path] = {};
+      }
+      deps.requires[path][require] = true;
+    }
+  }
+};
+
+
+// MOE:begin_strip
+/**
+ * Whether goog.require should throw an exception if it fails.
+ * @type {boolean}
+ */
+goog.useStrictRequires = false;
+
+
+// MOE:end_strip
+
+
+// NOTE(nnaze): The debug DOM loader was included in base.js as an original way
+// to do "debug-mode" development.  The dependency system can sometimes be
+// confusing, as can the debug DOM loader's asynchronous nature.
+//
+// With the DOM loader, a call to goog.require() is not blocking -- the script
+// will not load until some point after the current script.  If a namespace is
+// needed at runtime, it needs to be defined in a previous script, or loaded via
+// require() with its registered dependencies.
+//
+// User-defined namespaces may need their own deps file. For a reference on
+// creating a deps file, see:
+// MOE:begin_strip
+// Internally: http://go/deps-files and http://go/be#js_deps
+// MOE:end_strip
+// Externally: https://developers.google.com/closure/library/docs/depswriter
+//
+// Because of legacy clients, the DOM loader can't be easily removed from
+// base.js.  Work was done to make it disableable or replaceable for
+// different environments (DOM-less JavaScript interpreters like Rhino or V8,
+// for example). See bootstrap/ for more information.
+
+
+/**
+ * @define {boolean} Whether to enable the debug loader.
+ *
+ * If enabled, a call to goog.require() will attempt to load the namespace by
+ * appending a script tag to the DOM (if the namespace has been registered).
+ *
+ * If disabled, goog.require() will simply assert that the namespace has been
+ * provided (and depend on the fact that some outside tool correctly ordered
+ * the script).
+ */
+goog.define('goog.ENABLE_DEBUG_LOADER', true);
+
+
+/**
+ * @param {string} msg
+ * @private
+ */
+goog.logToConsole_ = function(msg) {
+  if (goog.global.console) {
+    goog.global.console['error'](msg);
+  }
+};
+
+
+/**
+ * Implements a system for the dynamic resolution of dependencies that works in
+ * parallel with the BUILD system. Note that all calls to goog.require will be
+ * stripped by the compiler.
+ * @see goog.provide
+ * @param {string} name Namespace to include (as was given in goog.provide()) in
+ *     the form "goog.package.part".
+ * @return {?} If called within a goog.module file, the associated namespace or
+ *     module otherwise null.
+ */
+goog.require = function(name) {
+  // If the object already exists we do not need to do anything.
+  if (!COMPILED) {
+    if (goog.ENABLE_DEBUG_LOADER && goog.IS_OLD_IE_) {
+      goog.maybeProcessDeferredDep_(name);
+    }
+
+    if (goog.isProvided_(name)) {
+      if (goog.isInModuleLoader_()) {
+        return goog.module.getInternal_(name);
+      }
+    } else if (goog.ENABLE_DEBUG_LOADER) {
+      var path = goog.getPathFromDeps_(name);
+      if (path) {
+        goog.writeScripts_(path);
+      } else {
+        var errorMessage = 'goog.require could not find: ' + name;
+        goog.logToConsole_(errorMessage);
+
+        // MOE:begin_strip
+
+        // NOTE(nicksantos): We could always throw an error, but this would
+        // break legacy users that depended on this failing silently. Instead,
+        // the compiler should warn us when there are invalid goog.require
+        // calls. For now, we simply give clients a way to turn strict mode on.
+        if (goog.useStrictRequires) {
+          throw new Error(errorMessage);
+        }
+
+        // In external Closure, always error.
+        // MOE:end_strip_and_replace throw new Error(errorMessage);
+      }
+    }
+
+    return null;
+  }
+};
+
+
+/**
+ * Path for included scripts.
+ * @type {string}
+ */
+goog.basePath = '';
+
+
+/**
+ * A hook for overriding the base path.
+ * @type {string|undefined}
+ */
+goog.global.CLOSURE_BASE_PATH;
+
+
+/**
+ * Whether to attempt to load Closure's deps file. By default, when uncompiled,
+ * deps files will attempt to be loaded.
+ * @type {boolean|undefined}
+ */
+goog.global.CLOSURE_NO_DEPS;
+
+
+/**
+ * A function to import a single script. This is meant to be overridden when
+ * Closure is being run in non-HTML contexts, such as web workers. It's defined
+ * in the global scope so that it can be set before base.js is loaded, which
+ * allows deps.js to be imported properly.
+ *
+ * The function is passed the script source, which is a relative URI. It should
+ * return true if the script was imported, false otherwise.
+ * @type {(function(string): boolean)|undefined}
+ */
+goog.global.CLOSURE_IMPORT_SCRIPT;
+
+
+/**
+ * Null function used for default values of callbacks, etc.
+ * @return {void} Nothing.
+ */
+goog.nullFunction = function() {};
+
+
+/**
+ * When defining a class Foo with an abstract method bar(), you can do:
+ * Foo.prototype.bar = goog.abstractMethod
+ *
+ * Now if a subclass of Foo fails to override bar(), an error will be thrown
+ * when bar() is invoked.
+ *
+ * @type {!Function}
+ * @throws {Error} when invoked to indicate the method should be overridden.
+ */
+goog.abstractMethod = function() {
+  throw new Error('unimplemented abstract method');
+};
+
+
+/**
+ * Adds a {@code getInstance} static method that always returns the same
+ * instance object.
+ * @param {!Function} ctor The constructor for the class to add the static
+ *     method to.
+ */
+goog.addSingletonGetter = function(ctor) {
+  // instance_ is immediately set to prevent issues with sealed constructors
+  // such as are encountered when a constructor is returned as the export object
+  // of a goog.module in unoptimized code.
+  ctor.instance_ = undefined;
+  ctor.getInstance = function() {
+    if (ctor.instance_) {
+      return ctor.instance_;
+    }
+    if (goog.DEBUG) {
+      // NOTE: JSCompiler can't optimize away Array#push.
+      goog.instantiatedSingletons_[goog.instantiatedSingletons_.length] = ctor;
+    }
+    return ctor.instance_ = new ctor;
+  };
+};
+
+
+/**
+ * All singleton classes that have been instantiated, for testing. Don't read
+ * it directly, use the {@code goog.testing.singleton} module. The compiler
+ * removes this variable if unused.
+ * @type {!Array<!Function>}
+ * @private
+ */
+goog.instantiatedSingletons_ = [];
+
+
+/**
+ * @define {boolean} Whether to load goog.modules using {@code eval} when using
+ * the debug loader.  This provides a better debugging experience as the
+ * source is unmodified and can be edited using Chrome Workspaces or similar.
+ * However in some environments the use of {@code eval} is banned
+ * so we provide an alternative.
+ */
+goog.define('goog.LOAD_MODULE_USING_EVAL', true);
+
+
+/**
+ * @define {boolean} Whether the exports of goog.modules should be sealed when
+ * possible.
+ */
+goog.define('goog.SEAL_MODULE_EXPORTS', goog.DEBUG);
+
+
+/**
+ * The registry of initialized modules:
+ * the module identifier to module exports map.
+ * @private @const {!Object<string, ?>}
+ */
+goog.loadedModules_ = {};
+
+
+/**
+ * True if goog.dependencies_ is available.
+ * @const {boolean}
+ */
+goog.DEPENDENCIES_ENABLED = !COMPILED && goog.ENABLE_DEBUG_LOADER;
+
+
+/**
+ * @define {string} How to decide whether to transpile.  Valid values
+ * are 'always', 'never', and 'detect'.  The default ('detect') is to
+ * use feature detection to determine which language levels need
+ * transpilation.
+ */
+// NOTE(sdh): we could expand this to accept a language level to bypass
+// detection: e.g. goog.TRANSPILE == 'es5' would transpile ES6 files but
+// would leave ES3 and ES5 files alone.
+goog.define('goog.TRANSPILE', 'detect');
+
+
+/**
+ * @define {string} Path to the transpiler.  Executing the script at this
+ * path (relative to base.js) should define a function $jscomp.transpile.
+ */
+goog.define('goog.TRANSPILER', 'transpile.js');
+
+
+if (goog.DEPENDENCIES_ENABLED) {
+  /**
+   * This object is used to keep track of dependencies and other data that is
+   * used for loading scripts.
+   * @private
+   * @type {{
+   *   loadFlags: !Object<string, !Object<string, string>>,
+   *   nameToPath: !Object<string, string>,
+   *   requires: !Object<string, !Object<string, boolean>>,
+   *   visited: !Object<string, boolean>,
+   *   written: !Object<string, boolean>,
+   *   deferred: !Object<string, string>
+   * }}
+   */
+  goog.dependencies_ = {
+    loadFlags: {},  // 1 to 1
+
+    nameToPath: {},  // 1 to 1
+
+    requires: {},  // 1 to many
+
+    // Used when resolving dependencies to prevent us from visiting file twice.
+    visited: {},
+
+    written: {},  // Used to keep track of script files we have written.
+
+    deferred: {}  // Used to track deferred module evaluations in old IEs
+  };
+
+
+  /**
+   * Tries to detect whether is in the context of an HTML document.
+   * @return {boolean} True if it looks like HTML document.
+   * @private
+   */
+  goog.inHtmlDocument_ = function() {
+    /** @type {Document} */
+    var doc = goog.global.document;
+    return doc != null && 'write' in doc;  // XULDocument misses write.
+  };
+
+
+  /**
+   * Tries to detect the base path of base.js script that bootstraps Closure.
+   * @private
+   */
+  goog.findBasePath_ = function() {
+    if (goog.isDef(goog.global.CLOSURE_BASE_PATH) &&
+        // Anti DOM-clobbering runtime check (b/37736576).
+        goog.isString(goog.global.CLOSURE_BASE_PATH)) {
+      goog.basePath = goog.global.CLOSURE_BASE_PATH;
+      return;
+    } else if (!goog.inHtmlDocument_()) {
+      return;
+    }
+    /** @type {Document} */
+    var doc = goog.global.document;
+    // If we have a currentScript available, use it exclusively.
+    var currentScript = doc.currentScript;
+    if (currentScript) {
+      var scripts = [currentScript];
+    } else {
+      var scripts = doc.getElementsByTagName('SCRIPT');
+    }
+    // Search backwards since the current script is in almost all cases the one
+    // that has base.js.
+    for (var i = scripts.length - 1; i >= 0; --i) {
+      var script = /** @type {!HTMLScriptElement} */ (scripts[i]);
+      var src = script.src;
+      var qmark = src.lastIndexOf('?');
+      var l = qmark == -1 ? src.length : qmark;
+      if (src.substr(l - 7, 7) == 'base.js') {
+        goog.basePath = src.substr(0, l - 7);
+        return;
+      }
+    }
+  };
+
+
+  /**
+   * Imports a script if, and only if, that script hasn't already been imported.
+   * (Must be called at execution time)
+   * @param {string} src Script source.
+   * @param {string=} opt_sourceText The optionally source text to evaluate
+   * @private
+   */
+  goog.importScript_ = function(src, opt_sourceText) {
+    var importScript =
+        goog.global.CLOSURE_IMPORT_SCRIPT || goog.writeScriptTag_;
+    if (importScript(src, opt_sourceText)) {
+      goog.dependencies_.written[src] = true;
+    }
+  };
+
+
+  /**
+   * Whether the browser is IE9 or earlier, which needs special handling
+   * for deferred modules.
+   * @const @private {boolean}
+   */
+  goog.IS_OLD_IE_ =
+      !!(!goog.global.atob && goog.global.document && goog.global.document.all);
+
+
+  /**
+   * Whether IE9 or earlier is waiting on a dependency.  This ensures that
+   * deferred modules that have no non-deferred dependencies actually get
+   * loaded, since if we defer them and then never pull in a non-deferred
+   * script, then `goog.loadQueuedModules_` will never be called.  Instead,
+   * if not waiting on anything we simply don't defer in the first place.
+   * @private {boolean}
+   */
+  goog.oldIeWaiting_ = false;
+
+
+  /**
+   * Given a URL initiate retrieval and execution of a script that needs
+   * pre-processing.
+   * @param {string} src Script source URL.
+   * @param {boolean} isModule Whether this is a goog.module.
+   * @param {boolean} needsTranspile Whether this source needs transpilation.
+   * @private
+   */
+  goog.importProcessedScript_ = function(src, isModule, needsTranspile) {
+    // In an attempt to keep browsers from timing out loading scripts using
+    // synchronous XHRs, put each load in its own script block.
+    var bootstrap = 'goog.retrieveAndExec_("' + src + '", ' + isModule + ', ' +
+        needsTranspile + ');';
+
+    goog.importScript_('', bootstrap);
+  };
+
+
+  /** @private {!Array<string>} */
+  goog.queuedModules_ = [];
+
+
+  /**
+   * Return an appropriate module text. Suitable to insert into
+   * a script tag (that is unescaped).
+   * @param {string} srcUrl
+   * @param {string} scriptText
+   * @return {string}
+   * @private
+   */
+  goog.wrapModule_ = function(srcUrl, scriptText) {
+    if (!goog.LOAD_MODULE_USING_EVAL || !goog.isDef(goog.global.JSON)) {
+      return '' +
+          'goog.loadModule(function(exports) {' +
+          '"use strict";' + scriptText +
+          '\n' +  // terminate any trailing single line comment.
+          ';return exports' +
+          '});' +
+          '\n//# sourceURL=' + srcUrl + '\n';
+    } else {
+      return '' +
+          'goog.loadModule(' +
+          goog.global.JSON.stringify(
+              scriptText + '\n//# sourceURL=' + srcUrl + '\n') +
+          ');';
+    }
+  };
+
+  // On IE9 and earlier, it is necessary to handle
+  // deferred module loads. In later browsers, the
+  // code to be evaluated is simply inserted as a script
+  // block in the correct order. To eval deferred
+  // code at the right time, we piggy back on goog.require to call
+  // goog.maybeProcessDeferredDep_.
+  //
+  // The goog.requires are used both to bootstrap
+  // the loading process (when no deps are available) and
+  // declare that they should be available.
+  //
+  // Here we eval the sources, if all the deps are available
+  // either already eval'd or goog.require'd.  This will
+  // be the case when all the dependencies have already
+  // been loaded, and the dependent module is loaded.
+  //
+  // But this alone isn't sufficient because it is also
+  // necessary to handle the case where there is no root
+  // that is not deferred.  For that there we register for an event
+  // and trigger goog.loadQueuedModules_ handle any remaining deferred
+  // evaluations.
+
+  /**
+   * Handle any remaining deferred goog.module evals.
+   * @private
+   */
+  goog.loadQueuedModules_ = function() {
+    var count = goog.queuedModules_.length;
+    if (count > 0) {
+      var queue = goog.queuedModules_;
+      goog.queuedModules_ = [];
+      for (var i = 0; i < count; i++) {
+        var path = queue[i];
+        goog.maybeProcessDeferredPath_(path);
+      }
+    }
+    goog.oldIeWaiting_ = false;
+  };
+
+
+  /**
+   * Eval the named module if its dependencies are
+   * available.
+   * @param {string} name The module to load.
+   * @private
+   */
+  goog.maybeProcessDeferredDep_ = function(name) {
+    if (goog.isDeferredModule_(name) && goog.allDepsAreAvailable_(name)) {
+      var path = goog.getPathFromDeps_(name);
+      goog.maybeProcessDeferredPath_(goog.basePath + path);
+    }
+  };
+
+  /**
+   * @param {string} name The module to check.
+   * @return {boolean} Whether the name represents a
+   *     module whose evaluation has been deferred.
+   * @private
+   */
+  goog.isDeferredModule_ = function(name) {
+    var path = goog.getPathFromDeps_(name);
+    var loadFlags = path && goog.dependencies_.loadFlags[path] || {};
+    var languageLevel = loadFlags['lang'] || 'es3';
+    if (path && (loadFlags['module'] == 'goog' ||
+                 goog.needsTranspile_(languageLevel))) {
+      var abspath = goog.basePath + path;
+      return (abspath) in goog.dependencies_.deferred;
+    }
+    return false;
+  };
+
+  /**
+   * @param {string} name The module to check.
+   * @return {boolean} Whether the name represents a
+   *     module whose declared dependencies have all been loaded
+   *     (eval'd or a deferred module load)
+   * @private
+   */
+  goog.allDepsAreAvailable_ = function(name) {
+    var path = goog.getPathFromDeps_(name);
+    if (path && (path in goog.dependencies_.requires)) {
+      for (var requireName in goog.dependencies_.requires[path]) {
+        if (!goog.isProvided_(requireName) &&
+            !goog.isDeferredModule_(requireName)) {
+          return false;
+        }
+      }
+    }
+    return true;
+  };
+
+
+  /**
+   * @param {string} abspath
+   * @private
+   */
+  goog.maybeProcessDeferredPath_ = function(abspath) {
+    if (abspath in goog.dependencies_.deferred) {
+      var src = goog.dependencies_.deferred[abspath];
+      delete goog.dependencies_.deferred[abspath];
+      goog.globalEval(src);
+    }
+  };
+
+
+  /**
+   * Load a goog.module from the provided URL.  This is not a general purpose
+   * code loader and does not support late loading code, that is it should only
+   * be used during page load. This method exists to support unit tests and
+   * "debug" loaders that would otherwise have inserted script tags. Under the
+   * hood this needs to use a synchronous XHR and is not recommeneded for
+   * production code.
+   *
+   * The module's goog.requires must have already been satisified; an exception
+   * will be thrown if this is not the case. This assumption is that no
+   * "deps.js" file exists, so there is no way to discover and locate the
+   * module-to-be-loaded's dependencies and no attempt is made to do so.
+   *
+   * There should only be one attempt to load a module.  If
+   * "goog.loadModuleFromUrl" is called for an already loaded module, an
+   * exception will be throw.
+   *
+   * @param {string} url The URL from which to attempt to load the goog.module.
+   */
+  goog.loadModuleFromUrl = function(url) {
+    // Because this executes synchronously, we don't need to do any additional
+    // bookkeeping. When "goog.loadModule" the namespace will be marked as
+    // having been provided which is sufficient.
+    goog.retrieveAndExec_(url, true, false);
+  };
+
+
+  /**
+   * Writes a new script pointing to {@code src} directly into the DOM.
+   *
+   * NOTE: This method is not CSP-compliant. @see goog.appendScriptSrcNode_ for
+   * the fallback mechanism.
+   *
+   * @param {string} src The script URL.
+   * @private
+   */
+  goog.writeScriptSrcNode_ = function(src) {
+    goog.global.document.write(
+        '<script type="text/javascript" src="' + src + '"></' +
+        'script>');
+  };
+
+
+  /**
+   * Appends a new script node to the DOM using a CSP-compliant mechanism. This
+   * method exists as a fallback for document.write (which is not allowed in a
+   * strict CSP context, e.g., Chrome apps).
+   *
+   * NOTE: This method is not analogous to using document.write to insert a
+   * <script> tag; specifically, the user agent will execute a script added by
+   * document.write immediately after the current script block finishes
+   * executing, whereas the DOM-appended script node will not be executed until
+   * the entire document is parsed and executed. That is to say, this script is
+   * added to the end of the script execution queue.
+   *
+   * The page must not attempt to call goog.required entities until after the
+   * document has loaded, e.g., in or after the window.onload callback.
+   *
+   * @param {string} src The script URL.
+   * @private
+   */
+  goog.appendScriptSrcNode_ = function(src) {
+    /** @type {Document} */
+    var doc = goog.global.document;
+    var scriptEl =
+        /** @type {HTMLScriptElement} */ (doc.createElement('script'));
+    scriptEl.type = 'text/javascript';
+    scriptEl.src = src;
+    scriptEl.defer = false;
+    scriptEl.async = false;
+    doc.head.appendChild(scriptEl);
+  };
+
+
+  /**
+   * The default implementation of the import function. Writes a script tag to
+   * import the script.
+   *
+   * @param {string} src The script url.
+   * @param {string=} opt_sourceText The optionally source text to evaluate
+   * @return {boolean} True if the script was imported, false otherwise.
+   * @private
+   */
+  goog.writeScriptTag_ = function(src, opt_sourceText) {
+    if (goog.inHtmlDocument_()) {
+      /** @type {!HTMLDocument} */
+      var doc = goog.global.document;
+
+      // If the user tries to require a new symbol after document load,
+      // something has gone terribly wrong. Doing a document.write would
+      // wipe out the page. This does not apply to the CSP-compliant method
+      // of writing script tags.
+      if (!goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING &&
+          doc.readyState == 'complete') {
+        // Certain test frameworks load base.js multiple times, which tries
+        // to write deps.js each time. If that happens, just fail silently.
+        // These frameworks wipe the page between each load of base.js, so this
+        // is OK.
+        var isDeps = /\bdeps.js$/.test(src);
+        if (isDeps) {
+          return false;
+        } else {
+          throw new Error('Cannot write "' + src + '" after document load');
+        }
+      }
+
+      if (opt_sourceText === undefined) {
+        if (!goog.IS_OLD_IE_) {
+          if (goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING) {
+            goog.appendScriptSrcNode_(src);
+          } else {
+            goog.writeScriptSrcNode_(src);
+          }
+        } else {
+          goog.oldIeWaiting_ = true;
+          var state = ' onreadystatechange=\'goog.onScriptLoad_(this, ' +
+              ++goog.lastNonModuleScriptIndex_ + ')\' ';
+          doc.write(
+              '<script type="text/javascript" src="' + src + '"' + state +
+              '></' +
+              'script>');
+        }
+      } else {
+        doc.write(
+            '<script type="text/javascript">' +
+            goog.protectScriptTag_(opt_sourceText) + '</' +
+            'script>');
+      }
+      return true;
+    } else {
+      return false;
+    }
+  };
+
+  /**
+   * Rewrites closing script tags in input to avoid ending an enclosing script
+   * tag.
+   *
+   * @param {string} str
+   * @return {string}
+   * @private
+   */
+  goog.protectScriptTag_ = function(str) {
+    return str.replace(/<\/(SCRIPT)/ig, '\\x3c/$1');
+  };
+
+  /**
+   * Determines whether the given language needs to be transpiled.
+   * @param {string} lang
+   * @return {boolean}
+   * @private
+   */
+  goog.needsTranspile_ = function(lang) {
+    if (goog.TRANSPILE == 'always') {
+      return true;
+    } else if (goog.TRANSPILE == 'never') {
+      return false;
+    } else if (!goog.requiresTranspilation_) {
+      goog.requiresTranspilation_ = goog.createRequiresTranspilation_();
+    }
+    if (lang in goog.requiresTranspilation_) {
+      return goog.requiresTranspilation_[lang];
+    } else {
+      throw new Error('Unknown language mode: ' + lang);
+    }
+  };
+
+  /** @private {?Object<string, boolean>} */
+  goog.requiresTranspilation_ = null;
+
+
+  /** @private {number} */
+  goog.lastNonModuleScriptIndex_ = 0;
+
+
+  /**
+   * A readystatechange handler for legacy IE
+   * @param {?} script
+   * @param {number} scriptIndex
+   * @return {boolean}
+   * @private
+   */
+  goog.onScriptLoad_ = function(script, scriptIndex) {
+    // for now load the modules when we reach the last script,
+    // later allow more inter-mingling.
+    if (script.readyState == 'complete' &&
+        goog.lastNonModuleScriptIndex_ == scriptIndex) {
+      goog.loadQueuedModules_();
+    }
+    return true;
+  };
+
+  /**
+   * Resolves dependencies based on the dependencies added using addDependency
+   * and calls importScript_ in the correct order.
+   * @param {string} pathToLoad The path from which to start discovering
+   *     dependencies.
+   * @private
+   */
+  goog.writeScripts_ = function(pathToLoad) {
+    /** @type {!Array<string>} The scripts we need to write this time. */
+    var scripts = [];
+    var seenScript = {};
+    var deps = goog.dependencies_;
+
+    /** @param {string} path */
+    function visitNode(path) {
+      if (path in deps.written) {
+        return;
+      }
+
+      // We have already visited this one. We can get here if we have cyclic
+      // dependencies.
+      if (path in deps.visited) {
+        return;
+      }
+
+      deps.visited[path] = true;
+
+      if (path in deps.requires) {
+        for (var requireName in deps.requires[path]) {
+          // If the required name is defined, we assume that it was already
+          // bootstrapped by other means.
+          if (!goog.isProvided_(requireName)) {
+            if (requireName in deps.nameToPath) {
+              visitNode(deps.nameToPath[requireName]);
+            } else {
+              throw new Error('Undefined nameToPath for ' + requireName);
+            }
+          }
+        }
+      }
+
+      if (!(path in seenScript)) {
+        seenScript[path] = true;
+        scripts.push(path);
+      }
+    }
+
+    visitNode(pathToLoad);
+
+    // record that we are going to load all these scripts.
+    for (var i = 0; i < scripts.length; i++) {
+      var path = scripts[i];
+      goog.dependencies_.written[path] = true;
+    }
+
+    // If a module is loaded synchronously then we need to
+    // clear the current inModuleLoader value, and restore it when we are
+    // done loading the current "requires".
+    var moduleState = goog.moduleLoaderState_;
+    goog.moduleLoaderState_ = null;
+
+    for (var i = 0; i < scripts.length; i++) {
+      var path = scripts[i];
+      if (path) {
+        var loadFlags = deps.loadFlags[path] || {};
+        var languageLevel = loadFlags['lang'] || 'es3';
+        var needsTranspile = goog.needsTranspile_(languageLevel);
+        if (loadFlags['module'] == 'goog' || needsTranspile) {
+          goog.importProcessedScript_(
+              goog.basePath + path, loadFlags['module'] == 'goog',
+              needsTranspile);
+        } else {
+          goog.importScript_(goog.basePath + path);
+        }
+      } else {
+        goog.moduleLoaderState_ = moduleState;
+        throw new Error('Undefined script input');
+      }
+    }
+
+    // restore the current "module loading state"
+    goog.moduleLoaderState_ = moduleState;
+  };
+
+
+  /**
+   * Looks at the dependency rules and tries to determine the script file that
+   * fulfills a particular rule.
+   * @param {string} rule In the form goog.namespace.Class or project.script.
+   * @return {?string} Url corresponding to the rule, or null.
+   * @private
+   */
+  goog.getPathFromDeps_ = function(rule) {
+    if (rule in goog.dependencies_.nameToPath) {
+      return goog.dependencies_.nameToPath[rule];
+    } else {
+      return null;
+    }
+  };
+
+  goog.findBasePath_();
+
+  // Allow projects to manage the deps files themselves.
+  if (!goog.global.CLOSURE_NO_DEPS) {
+    goog.importScript_(goog.basePath + 'deps.js');
+  }
+}
+
+
+/**
+ * @package {?boolean}
+ * Visible for testing.
+ */
+goog.hasBadLetScoping = null;
+
+
+/**
+ * @return {boolean}
+ * @package Visible for testing.
+ */
+goog.useSafari10Workaround = function() {
+  if (goog.hasBadLetScoping == null) {
+    var hasBadLetScoping;
+    try {
+      hasBadLetScoping = !eval(
+          '"use strict";' +
+          'let x = 1; function f() { return typeof x; };' +
+          'f() == "number";');
+    } catch (e) {
+      // Assume that ES6 syntax isn't supported.
+      hasBadLetScoping = false;
+    }
+    goog.hasBadLetScoping = hasBadLetScoping;
+  }
+  return goog.hasBadLetScoping;
+};
+
+
+/**
+ * @param {string} moduleDef
+ * @return {string}
+ * @package Visible for testing.
+ */
+goog.workaroundSafari10EvalBug = function(moduleDef) {
+  return '(function(){' + moduleDef +
+      '\n' +  // Terminate any trailing single line comment.
+      ';' +   // Terminate any trailing expression.
+      '})();\n';
+};
+
+
+/**
+ * @param {function(?):?|string} moduleDef The module definition.
+ */
+goog.loadModule = function(moduleDef) {
+  // NOTE: we allow function definitions to be either in the from
+  // of a string to eval (which keeps the original source intact) or
+  // in a eval forbidden environment (CSP) we allow a function definition
+  // which in its body must call {@code goog.module}, and return the exports
+  // of the module.
+  var previousState = goog.moduleLoaderState_;
+  try {
+    goog.moduleLoaderState_ = {
+      moduleName: undefined,
+      declareLegacyNamespace: false
+    };
+    var exports;
+    if (goog.isFunction(moduleDef)) {
+      exports = moduleDef.call(undefined, {});
+    } else if (goog.isString(moduleDef)) {
+      if (goog.useSafari10Workaround()) {
+        moduleDef = goog.workaroundSafari10EvalBug(moduleDef);
+      }
+
+      exports = goog.loadModuleFromSource_.call(undefined, moduleDef);
+    } else {
+      throw new Error('Invalid module definition');
+    }
+
+    var moduleName = goog.moduleLoaderState_.moduleName;
+    if (!goog.isString(moduleName) || !moduleName) {
+      throw new Error('Invalid module name \"' + moduleName + '\"');
+    }
+
+    // Don't seal legacy namespaces as they may be uses as a parent of
+    // another namespace
+    if (goog.moduleLoaderState_.declareLegacyNamespace) {
+      goog.constructNamespace_(moduleName, exports);
+    } else if (
+        goog.SEAL_MODULE_EXPORTS && Object.seal && typeof exports == 'object' &&
+        exports != null) {
+      Object.seal(exports);
+    }
+
+    goog.loadedModules_[moduleName] = exports;
+  } finally {
+    goog.moduleLoaderState_ = previousState;
+  }
+};
+
+
+/**
+ * @private @const
+ */
+goog.loadModuleFromSource_ = /** @type {function(string):?} */ (function() {
+  // NOTE: we avoid declaring parameters or local variables here to avoid
+  // masking globals or leaking values into the module definition.
+  'use strict';
+  var exports = {};
+  eval(arguments[0]);
+  return exports;
+});
+
+
+/**
+ * Normalize a file path by removing redundant ".." and extraneous "." file
+ * path components.
+ * @param {string} path
+ * @return {string}
+ * @private
+ */
+goog.normalizePath_ = function(path) {
+  var components = path.split('/');
+  var i = 0;
+  while (i < components.length) {
+    if (components[i] == '.') {
+      components.splice(i, 1);
+    } else if (
+        i && components[i] == '..' && components[i - 1] &&
+        components[i - 1] != '..') {
+      components.splice(--i, 2);
+    } else {
+      i++;
+    }
+  }
+  return components.join('/');
+};
+
+
+/**
+ * Provides a hook for loading a file when using Closure's goog.require() API
+ * with goog.modules.  In particular this hook is provided to support Node.js.
+ *
+ * @type {(function(string):string)|undefined}
+ */
+goog.global.CLOSURE_LOAD_FILE_SYNC;
+
+
+/**
+ * Loads file by synchronous XHR. Should not be used in production environments.
+ * @param {string} src Source URL.
+ * @return {?string} File contents, or null if load failed.
+ * @private
+ */
+goog.loadFileSync_ = function(src) {
+  if (goog.global.CLOSURE_LOAD_FILE_SYNC) {
+    return goog.global.CLOSURE_LOAD_FILE_SYNC(src);
+  } else {
+    try {
+      /** @type {XMLHttpRequest} */
+      var xhr = new goog.global['XMLHttpRequest']();
+      xhr.open('get', src, false);
+      xhr.send();
+      // NOTE: Successful http: requests have a status of 200, but successful
+      // file: requests may have a status of zero.  Any other status, or a
+      // thrown exception (particularly in case of file: requests) indicates
+      // some sort of error, which we treat as a missing or unavailable file.
+      return xhr.status == 0 || xhr.status == 200 ? xhr.responseText : null;
+    } catch (err) {
+      // No need to rethrow or log, since errors should show up on their own.
+      return null;
+    }
+  }
+};
+
+
+/**
+ * Retrieve and execute a script that needs some sort of wrapping.
+ * @param {string} src Script source URL.
+ * @param {boolean} isModule Whether to load as a module.
+ * @param {boolean} needsTranspile Whether to transpile down to ES3.
+ * @private
+ */
+goog.retrieveAndExec_ = function(src, isModule, needsTranspile) {
+  if (!COMPILED) {
+    // The full but non-canonicalized URL for later use.
+    var originalPath = src;
+    // Canonicalize the path, removing any /./ or /../ since Chrome's debugging
+    // console doesn't auto-canonicalize XHR loads as it does <script> srcs.
+    src = goog.normalizePath_(src);
+
+    var importScript =
+        goog.global.CLOSURE_IMPORT_SCRIPT || goog.writeScriptTag_;
+
+    var scriptText = goog.loadFileSync_(src);
+    if (scriptText == null) {
+      throw new Error('Load of "' + src + '" failed');
+    }
+
+    if (needsTranspile) {
+      scriptText = goog.transpile_.call(goog.global, scriptText, src);
+    }
+
+    if (isModule) {
+      scriptText = goog.wrapModule_(src, scriptText);
+    } else {
+      scriptText += '\n//# sourceURL=' + src;
+    }
+    var isOldIE = goog.IS_OLD_IE_;
+    if (isOldIE && goog.oldIeWaiting_) {
+      goog.dependencies_.deferred[originalPath] = scriptText;
+      goog.queuedModules_.push(originalPath);
+    } else {
+      importScript(src, scriptText);
+    }
+  }
+};
+
+
+/**
+ * Lazily retrieves the transpiler and applies it to the source.
+ * @param {string} code JS code.
+ * @param {string} path Path to the code.
+ * @return {string} The transpiled code.
+ * @private
+ */
+goog.transpile_ = function(code, path) {
+  var jscomp = goog.global['$jscomp'];
+  if (!jscomp) {
+    goog.global['$jscomp'] = jscomp = {};
+  }
+  var transpile = jscomp.transpile;
+  if (!transpile) {
+    var transpilerPath = goog.basePath + goog.TRANSPILER;
+    var transpilerCode = goog.loadFileSync_(transpilerPath);
+    if (transpilerCode) {
+      // This must be executed synchronously, since by the time we know we
+      // need it, we're about to load and write the ES6 code synchronously,
+      // so a normal script-tag load will be too slow.
+      eval(transpilerCode + '\n//# sourceURL=' + transpilerPath);
+      // Even though the transpiler is optional, if $gwtExport is found, it's
+      // a sign the transpiler was loaded and the $jscomp.transpile *should*
+      // be there.
+      if (goog.global['$gwtExport'] && goog.global['$gwtExport']['$jscomp'] &&
+          !goog.global['$gwtExport']['$jscomp']['transpile']) {
+        throw new Error(
+            'The transpiler did not properly export the "transpile" ' +
+            'method. $gwtExport: ' + JSON.stringify(goog.global['$gwtExport']));
+      }
+      // transpile.js only exports a single $jscomp function, transpile. We
+      // grab just that and add it to the existing definition of $jscomp which
+      // contains the polyfills.
+      goog.global['$jscomp'].transpile =
+          goog.global['$gwtExport']['$jscomp']['transpile'];
+      jscomp = goog.global['$jscomp'];
+      transpile = jscomp.transpile;
+    }
+  }
+  if (!transpile) {
+    // The transpiler is an optional component.  If it's not available then
+    // replace it with a pass-through function that simply logs.
+    var suffix = ' requires transpilation but no transpiler was found.';
+    // MOE:begin_strip
+    suffix +=  // Provide a more appropriate message internally.
+        ' Please add "//javascript/closure:transpiler" as a data ' +
+        'dependency to ensure it is included.';
+    // MOE:end_strip
+    transpile = jscomp.transpile = function(code, path) {
+      // TODO(sdh): figure out some way to get this error to show up
+      // in test results, noting that the failure may occur in many
+      // different ways, including in loadModule() before the test
+      // runner even comes up.
+      goog.logToConsole_(path + suffix);
+      return code;
+    };
+  }
+  // Note: any transpilation errors/warnings will be logged to the console.
+  return transpile(code, path);
+};
+
+
+//==============================================================================
+// Language Enhancements
+//==============================================================================
+
+
+/**
+ * This is a "fixed" version of the typeof operator.  It differs from the typeof
+ * operator in such a way that null returns 'null' and arrays return 'array'.
+ * @param {?} value The value to get the type of.
+ * @return {string} The name of the type.
+ */
+goog.typeOf = function(value) {
+  var s = typeof value;
+  if (s == 'object') {
+    if (value) {
+      // Check these first, so we can avoid calling Object.prototype.toString if
+      // possible.
+      //
+      // IE improperly marshals typeof across execution contexts, but a
+      // cross-context object will still return false for "instanceof Object".
+      if (value instanceof Array) {
+        return 'array';
+      } else if (value instanceof Object) {
+        return s;
+      }
+
+      // HACK: In order to use an Object prototype method on the arbitrary
+      //   value, the compiler requires the value be cast to type Object,
+      //   even though the ECMA spec explicitly allows it.
+      var className = Object.prototype.toString.call(
+          /** @type {!Object} */ (value));
+      // In Firefox 3.6, attempting to access iframe window objects' length
+      // property throws an NS_ERROR_FAILURE, so we need to special-case it
+      // here.
+      if (className == '[object Window]') {
+        return 'object';
+      }
+
+      // We cannot always use constructor == Array or instanceof Array because
+      // different frames have different Array objects. In IE6, if the iframe
+      // where the array was created is destroyed, the array loses its
+      // prototype. Then dereferencing val.splice here throws an exception, so
+      // we can't use goog.isFunction. Calling typeof directly returns 'unknown'
+      // so that will work. In this case, this function will return false and
+      // most array functions will still work because the array is still
+      // array-like (supports length and []) even though it has lost its
+      // prototype.
+      // Mark Miller noticed that Object.prototype.toString
+      // allows access to the unforgeable [[Class]] property.
+      //  15.2.4.2 Object.prototype.toString ( )
+      //  When the toString method is called, the following steps are taken:
+      //      1. Get the [[Class]] property of this object.
+      //      2. Compute a string value by concatenating the three strings
+      //         "[object ", Result(1), and "]".
+      //      3. Return Result(2).
+      // and this behavior survives the destruction of the execution context.
+      if ((className == '[object Array]' ||
+           // In IE all non value types are wrapped as objects across window
+           // boundaries (not iframe though) so we have to do object detection
+           // for this edge case.
+           typeof value.length == 'number' &&
+               typeof value.splice != 'undefined' &&
+               typeof value.propertyIsEnumerable != 'undefined' &&
+               !value.propertyIsEnumerable('splice')
+
+               )) {
+        return 'array';
+      }
+      // HACK: There is still an array case that fails.
+      //     function ArrayImpostor() {}
+      //     ArrayImpostor.prototype = [];
+      //     var impostor = new ArrayImpostor;
+      // this can be fixed by getting rid of the fast path
+      // (value instanceof Array) and solely relying on
+      // (value && Object.prototype.toString.vall(value) === '[object Array]')
+      // but that would require many more function calls and is not warranted
+      // unless closure code is receiving objects from untrusted sources.
+
+      // IE in cross-window calls does not correctly marshal the function type
+      // (it appears just as an object) so we cannot use just typeof val ==
+      // 'function'. However, if the object has a call property, it is a
+      // function.
+      if ((className == '[object Function]' ||
+           typeof value.call != 'undefined' &&
+               typeof value.propertyIsEnumerable != 'undefined' &&
+               !value.propertyIsEnumerable('call'))) {
+        return 'function';
+      }
+
+    } else {
+      return 'null';
+    }
+
+  } else if (s == 'function' && typeof value.call == 'undefined') {
+    // In Safari typeof nodeList returns 'function', and on Firefox typeof
+    // behaves similarly for HTML{Applet,Embed,Object}, Elements and RegExps. We
+    // would like to return object for those and we can detect an invalid
+    // function by making sure that the function object has a call method.
+    return 'object';
+  }
+  return s;
+};
+
+
+/**
+ * Returns true if the specified value is null.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is null.
+ */
+goog.isNull = function(val) {
+  return val === null;
+};
+
+
+/**
+ * Returns true if the specified value is defined and not null.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is defined and not null.
+ */
+goog.isDefAndNotNull = function(val) {
+  // Note that undefined == null.
+  return val != null;
+};
+
+
+/**
+ * Returns true if the specified value is an array.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is an array.
+ */
+goog.isArray = function(val) {
+  return goog.typeOf(val) == 'array';
+};
+
+
+/**
+ * Returns true if the object looks like an array. To qualify as array like
+ * the value needs to be either a NodeList or an object with a Number length
+ * property. As a special case, a function value is not array like, because its
+ * length property is fixed to correspond to the number of expected arguments.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is an array.
+ */
+goog.isArrayLike = function(val) {
+  var type = goog.typeOf(val);
+  // We do not use goog.isObject here in order to exclude function values.
+  return type == 'array' || type == 'object' && typeof val.length == 'number';
+};
+
+
+/**
+ * Returns true if the object looks like a Date. To qualify as Date-like the
+ * value needs to be an object and have a getFullYear() function.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is a like a Date.
+ */
+goog.isDateLike = function(val) {
+  return goog.isObject(val) && typeof val.getFullYear == 'function';
+};
+
+
+/**
+ * Returns true if the specified value is a function.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is a function.
+ */
+goog.isFunction = function(val) {
+  return goog.typeOf(val) == 'function';
+};
+
+
+/**
+ * Returns true if the specified value is an object.  This includes arrays and
+ * functions.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is an object.
+ */
+goog.isObject = function(val) {
+  var type = typeof val;
+  return type == 'object' && val != null || type == 'function';
+  // return Object(val) === val also works, but is slower, especially if val is
+  // not an object.
+};
+
+
+/**
+ * Gets a unique ID for an object. This mutates the object so that further calls
+ * with the same object as a parameter returns the same value. The unique ID is
+ * guaranteed to be unique across the current session amongst objects that are
+ * passed into {@code getUid}. There is no guarantee that the ID is unique or
+ * consistent across sessions. It is unsafe to generate unique ID for function
+ * prototypes.
+ *
+ * @param {Object} obj The object to get the unique ID for.
+ * @return {number} The unique ID for the object.
+ */
+goog.getUid = function(obj) {
+  // TODO(arv): Make the type stricter, do not accept null.
+
+  // In Opera window.hasOwnProperty exists but always returns false so we avoid
+  // using it. As a consequence the unique ID generated for BaseClass.prototype
+  // and SubClass.prototype will be the same.
+  return obj[goog.UID_PROPERTY_] ||
+      (obj[goog.UID_PROPERTY_] = ++goog.uidCounter_);
+};
+
+
+/**
+ * Whether the given object is already assigned a unique ID.
+ *
+ * This does not modify the object.
+ *
+ * @param {!Object} obj The object to check.
+ * @return {boolean} Whether there is an assigned unique id for the object.
+ */
+goog.hasUid = function(obj) {
+  return !!obj[goog.UID_PROPERTY_];
+};
+
+
+/**
+ * Removes the unique ID from an object. This is useful if the object was
+ * previously mutated using {@code goog.getUid} in which case the mutation is
+ * undone.
+ * @param {Object} obj The object to remove the unique ID field from.
+ */
+goog.removeUid = function(obj) {
+  // TODO(arv): Make the type stricter, do not accept null.
+
+  // In IE, DOM nodes are not instances of Object and throw an exception if we
+  // try to delete.  Instead we try to use removeAttribute.
+  if (obj !== null && 'removeAttribute' in obj) {
+    obj.removeAttribute(goog.UID_PROPERTY_);
+  }
+
+  try {
+    delete obj[goog.UID_PROPERTY_];
+  } catch (ex) {
+  }
+};
+
+
+/**
+ * Name for unique ID property. Initialized in a way to help avoid collisions
+ * with other closure JavaScript on the same page.
+ * @type {string}
+ * @private
+ */
+goog.UID_PROPERTY_ = 'closure_uid_' + ((Math.random() * 1e9) >>> 0);
+
+
+/**
+ * Counter for UID.
+ * @type {number}
+ * @private
+ */
+goog.uidCounter_ = 0;
+
+
+/**
+ * Adds a hash code field to an object. The hash code is unique for the
+ * given object.
+ * @param {Object} obj The object to get the hash code for.
+ * @return {number} The hash code for the object.
+ * @deprecated Use goog.getUid instead.
+ */
+goog.getHashCode = goog.getUid;
+
+
+/**
+ * Removes the hash code field from an object.
+ * @param {Object} obj The object to remove the field from.
+ * @deprecated Use goog.removeUid instead.
+ */
+goog.removeHashCode = goog.removeUid;
+
+
+/**
+ * Clones a value. The input may be an Object, Array, or basic type. Objects and
+ * arrays will be cloned recursively.
+ *
+ * WARNINGS:
+ * <code>goog.cloneObject</code> does not detect reference loops. Objects that
+ * refer to themselves will cause infinite recursion.
+ *
+ * <code>goog.cloneObject</code> is unaware of unique identifiers, and copies
+ * UIDs created by <code>getUid</code> into cloned results.
+ *
+ * @param {*} obj The value to clone.
+ * @return {*} A clone of the input value.
+ * @deprecated goog.cloneObject is unsafe. Prefer the goog.object methods.
+ */
+goog.cloneObject = function(obj) {
+  var type = goog.typeOf(obj);
+  if (type == 'object' || type == 'array') {
+    if (obj.clone) {
+      return obj.clone();
+    }
+    var clone = type == 'array' ? [] : {};
+    for (var key in obj) {
+      clone[key] = goog.cloneObject(obj[key]);
+    }
+    return clone;
+  }
+
+  return obj;
+};
+
+
+/**
+ * A native implementation of goog.bind.
+ * @param {?function(this:T, ...)} fn A function to partially apply.
+ * @param {T} selfObj Specifies the object which this should point to when the
+ *     function is run.
+ * @param {...*} var_args Additional arguments that are partially applied to the
+ *     function.
+ * @return {!Function} A partially-applied form of the function goog.bind() was
+ *     invoked as a method of.
+ * @template T
+ * @private
+ */
+goog.bindNative_ = function(fn, selfObj, var_args) {
+  return /** @type {!Function} */ (fn.call.apply(fn.bind, arguments));
+};
+
+
+/**
+ * A pure-JS implementation of goog.bind.
+ * @param {?function(this:T, ...)} fn A function to partially apply.
+ * @param {T} selfObj Specifies the object which this should point to when the
+ *     function is run.
+ * @param {...*} var_args Additional arguments that are partially applied to the
+ *     function.
+ * @return {!Function} A partially-applied form of the function goog.bind() was
+ *     invoked as a method of.
+ * @template T
+ * @private
+ */
+goog.bindJs_ = function(fn, selfObj, var_args) {
+  if (!fn) {
+    throw new Error();
+  }
+
+  if (arguments.length > 2) {
+    var boundArgs = Array.prototype.slice.call(arguments, 2);
+    return function() {
+      // Prepend the bound arguments to the current arguments.
+      var newArgs = Array.prototype.slice.call(arguments);
+      Array.prototype.unshift.apply(newArgs, boundArgs);
+      return fn.apply(selfObj, newArgs);
+    };
+
+  } else {
+    return function() {
+      return fn.apply(selfObj, arguments);
+    };
+  }
+};
+
+
+/**
+ * Partially applies this function to a particular 'this object' and zero or
+ * more arguments. The result is a new function with some arguments of the first
+ * function pre-filled and the value of this 'pre-specified'.
+ *
+ * Remaining arguments specified at call-time are appended to the pre-specified
+ * ones.
+ *
+ * Also see: {@link #partial}.
+ *
+ * Usage:
+ * <pre>var barMethBound = goog.bind(myFunction, myObj, 'arg1', 'arg2');
+ * barMethBound('arg3', 'arg4');</pre>
+ *
+ * @param {?function(this:T, ...)} fn A function to partially apply.
+ * @param {T} selfObj Specifies the object which this should point to when the
+ *     function is run.
+ * @param {...*} var_args Additional arguments that are partially applied to the
+ *     function.
+ * @return {!Function} A partially-applied form of the function goog.bind() was
+ *     invoked as a method of.
+ * @template T
+ * @suppress {deprecated} See above.
+ */
+goog.bind = function(fn, selfObj, var_args) {
+  // TODO(nicksantos): narrow the type signature.
+  if (Function.prototype.bind &&
+      // NOTE(nicksantos): Somebody pulled base.js into the default Chrome
+      // extension environment. This means that for Chrome extensions, they get
+      // the implementation of Function.prototype.bind that calls goog.bind
+      // instead of the native one. Even worse, we don't want to introduce a
+      // circular dependency between goog.bind and Function.prototype.bind, so
+      // we have to hack this to make sure it works correctly.
+      Function.prototype.bind.toString().indexOf('native code') != -1) {
+    goog.bind = goog.bindNative_;
+  } else {
+    goog.bind = goog.bindJs_;
+  }
+  return goog.bind.apply(null, arguments);
+};
+
+
+/**
+ * Like goog.bind(), except that a 'this object' is not required. Useful when
+ * the target function is already bound.
+ *
+ * Usage:
+ * var g = goog.partial(f, arg1, arg2);
+ * g(arg3, arg4);
+ *
+ * @param {Function} fn A function to partially apply.
+ * @param {...*} var_args Additional arguments that are partially applied to fn.
+ * @return {!Function} A partially-applied form of the function goog.partial()
+ *     was invoked as a method of.
+ */
+goog.partial = function(fn, var_args) {
+  var args = Array.prototype.slice.call(arguments, 1);
+  return function() {
+    // Clone the array (with slice()) and append additional arguments
+    // to the existing arguments.
+    var newArgs = args.slice();
+    newArgs.push.apply(newArgs, arguments);
+    return fn.apply(this, newArgs);
+  };
+};
+
+
+/**
+ * Copies all the members of a source object to a target object. This method
+ * does not work on all browsers for all objects that contain keys such as
+ * toString or hasOwnProperty. Use goog.object.extend for this purpose.
+ * @param {Object} target Target.
+ * @param {Object} source Source.
+ */
+goog.mixin = function(target, source) {
+  for (var x in source) {
+    target[x] = source[x];
+  }
+
+  // For IE7 or lower, the for-in-loop does not contain any properties that are
+  // not enumerable on the prototype object (for example, isPrototypeOf from
+  // Object.prototype) but also it will not include 'replace' on objects that
+  // extend String and change 'replace' (not that it is common for anyone to
+  // extend anything except Object).
+};
+
+
+/**
+ * @return {number} An integer value representing the number of milliseconds
+ *     between midnight, January 1, 1970 and the current time.
+ */
+goog.now = (goog.TRUSTED_SITE && Date.now) || (function() {
+             // Unary plus operator converts its operand to a number which in
+             // the case of
+             // a date is done by calling getTime().
+             return +new Date();
+           });
+
+
+/**
+ * Evals JavaScript in the global scope.  In IE this uses execScript, other
+ * browsers use goog.global.eval. If goog.global.eval does not evaluate in the
+ * global scope (for example, in Safari), appends a script tag instead.
+ * Throws an exception if neither execScript or eval is defined.
+ * @param {string} script JavaScript string.
+ */
+goog.globalEval = function(script) {
+  if (goog.global.execScript) {
+    goog.global.execScript(script, 'JavaScript');
+  } else if (goog.global.eval) {
+    // Test to see if eval works
+    if (goog.evalWorksForGlobals_ == null) {
+      goog.global.eval('var _evalTest_ = 1;');
+      if (typeof goog.global['_evalTest_'] != 'undefined') {
+        try {
+          delete goog.global['_evalTest_'];
+        } catch (ignore) {
+          // Microsoft edge fails the deletion above in strict mode.
+        }
+        goog.evalWorksForGlobals_ = true;
+      } else {
+        goog.evalWorksForGlobals_ = false;
+      }
+    }
+
+    if (goog.evalWorksForGlobals_) {
+      goog.global.eval(script);
+    } else {
+      /** @type {Document} */
+      var doc = goog.global.document;
+      var scriptElt =
+          /** @type {!HTMLScriptElement} */ (doc.createElement('SCRIPT'));
+      scriptElt.type = 'text/javascript';
+      scriptElt.defer = false;
+      // Note(pupius): can't use .innerHTML since "t('<test>')" will fail and
+      // .text doesn't work in Safari 2.  Therefore we append a text node.
+      scriptElt.appendChild(doc.createTextNode(script));
+      doc.body.appendChild(scriptElt);
+      doc.body.removeChild(scriptElt);
+    }
+  } else {
+    throw new Error('goog.globalEval not available');
+  }
+};
+
+
+/**
+ * Indicates whether or not we can call 'eval' directly to eval code in the
+ * global scope. Set to a Boolean by the first call to goog.globalEval (which
+ * empirically tests whether eval works for globals). @see goog.globalEval
+ * @type {?boolean}
+ * @private
+ */
+goog.evalWorksForGlobals_ = null;
+
+
+/**
+ * Optional map of CSS class names to obfuscated names used with
+ * goog.getCssName().
+ * @private {!Object<string, string>|undefined}
+ * @see goog.setCssNameMapping
+ */
+goog.cssNameMapping_;
+
+
+/**
+ * Optional obfuscation style for CSS class names. Should be set to either
+ * 'BY_WHOLE' or 'BY_PART' if defined.
+ * @type {string|undefined}
+ * @private
+ * @see goog.setCssNameMapping
+ */
+goog.cssNameMappingStyle_;
+
+
+
+/**
+ * A hook for modifying the default behavior goog.getCssName. The function
+ * if present, will recieve the standard output of the goog.getCssName as
+ * its input.
+ *
+ * @type {(function(string):string)|undefined}
+ */
+goog.global.CLOSURE_CSS_NAME_MAP_FN;
+
+
+/**
+ * Handles strings that are intended to be used as CSS class names.
+ *
+ * This function works in tandem with @see goog.setCssNameMapping.
+ *
+ * Without any mapping set, the arguments are simple joined with a hyphen and
+ * passed through unaltered.
+ *
+ * When there is a mapping, there are two possible styles in which these
+ * mappings are used. In the BY_PART style, each part (i.e. in between hyphens)
+ * of the passed in css name is rewritten according to the map. In the BY_WHOLE
+ * style, the full css name is looked up in the map directly. If a rewrite is
+ * not specified by the map, the compiler will output a warning.
+ *
+ * When the mapping is passed to the compiler, it will replace calls to
+ * goog.getCssName with the strings from the mapping, e.g.
+ *     var x = goog.getCssName('foo');
+ *     var y = goog.getCssName(this.baseClass, 'active');
+ *  becomes:
+ *     var x = 'foo';
+ *     var y = this.baseClass + '-active';
+ *
+ * If one argument is passed it will be processed, if two are passed only the
+ * modifier will be processed, as it is assumed the first argument was generated
+ * as a result of calling goog.getCssName.
+ *
+ * @param {string} className The class name.
+ * @param {string=} opt_modifier A modifier to be appended to the class name.
+ * @return {string} The class name or the concatenation of the class name and
+ *     the modifier.
+ */
+goog.getCssName = function(className, opt_modifier) {
+  // String() is used for compatibility with compiled soy where the passed
+  // className can be non-string objects.
+  if (String(className).charAt(0) == '.') {
+    throw new Error(
+        'className passed in goog.getCssName must not start with ".".' +
+        ' You passed: ' + className);
+  }
+
+  var getMapping = function(cssName) {
+    return goog.cssNameMapping_[cssName] || cssName;
+  };
+
+  var renameByParts = function(cssName) {
+    // Remap all the parts individually.
+    var parts = cssName.split('-');
+    var mapped = [];
+    for (var i = 0; i < parts.length; i++) {
+      mapped.push(getMapping(parts[i]));
+    }
+    return mapped.join('-');
+  };
+
+  var rename;
+  if (goog.cssNameMapping_) {
+    rename =
+        goog.cssNameMappingStyle_ == 'BY_WHOLE' ? getMapping : renameByParts;
+  } else {
+    rename = function(a) {
+      return a;
+    };
+  }
+
+  var result =
+      opt_modifier ? className + '-' + rename(opt_modifier) : rename(className);
+
+  // The special CLOSURE_CSS_NAME_MAP_FN allows users to specify further
+  // processing of the class name.
+  if (goog.global.CLOSURE_CSS_NAME_MAP_FN) {
+    return goog.global.CLOSURE_CSS_NAME_MAP_FN(result);
+  }
+
+  return result;
+};
+
+
+/**
+ * Sets the map to check when returning a value from goog.getCssName(). Example:
+ * <pre>
+ * goog.setCssNameMapping({
+ *   "goog": "a",
+ *   "disabled": "b",
+ * });
+ *
+ * var x = goog.getCssName('goog');
+ * // The following evaluates to: "a a-b".
+ * goog.getCssName('goog') + ' ' + goog.getCssName(x, 'disabled')
+ * </pre>
+ * When declared as a map of string literals to string literals, the JSCompiler
+ * will replace all calls to goog.getCssName() using the supplied map if the
+ * --process_closure_primitives flag is set.
+ *
+ * @param {!Object} mapping A map of strings to strings where keys are possible
+ *     arguments to goog.getCssName() and values are the corresponding values
+ *     that should be returned.
+ * @param {string=} opt_style The style of css name mapping. There are two valid
+ *     options: 'BY_PART', and 'BY_WHOLE'.
+ * @see goog.getCssName for a description.
+ */
+goog.setCssNameMapping = function(mapping, opt_style) {
+  goog.cssNameMapping_ = mapping;
+  goog.cssNameMappingStyle_ = opt_style;
+};
+
+
+/**
+ * To use CSS renaming in compiled mode, one of the input files should have a
+ * call to goog.setCssNameMapping() with an object literal that the JSCompiler
+ * can extract and use to replace all calls to goog.getCssName(). In uncompiled
+ * mode, JavaScript code should be loaded before this base.js file that declares
+ * a global variable, CLOSURE_CSS_NAME_MAPPING, which is used below. This is
+ * to ensure that the mapping is loaded before any calls to goog.getCssName()
+ * are made in uncompiled mode.
+ *
+ * A hook for overriding the CSS name mapping.
+ * @type {!Object<string, string>|undefined}
+ */
+goog.global.CLOSURE_CSS_NAME_MAPPING;
+
+
+if (!COMPILED && goog.global.CLOSURE_CSS_NAME_MAPPING) {
+  // This does not call goog.setCssNameMapping() because the JSCompiler
+  // requires that goog.setCssNameMapping() be called with an object literal.
+  goog.cssNameMapping_ = goog.global.CLOSURE_CSS_NAME_MAPPING;
+}
+
+
+/**
+ * Gets a localized message.
+ *
+ * This function is a compiler primitive. If you give the compiler a localized
+ * message bundle, it will replace the string at compile-time with a localized
+ * version, and expand goog.getMsg call to a concatenated string.
+ *
+ * Messages must be initialized in the form:
+ * <code>
+ * var MSG_NAME = goog.getMsg('Hello {$placeholder}', {'placeholder': 'world'});
+ * </code>
+ *
+ * This function produces a string which should be treated as plain text. Use
+ * {@link goog.html.SafeHtmlFormatter} in conjunction with goog.getMsg to
+ * produce SafeHtml.
+ *
+ * @param {string} str Translatable string, places holders in the form {$foo}.
+ * @param {Object<string, string>=} opt_values Maps place holder name to value.
+ * @return {string} message with placeholders filled.
+ */
+goog.getMsg = function(str, opt_values) {
+  if (opt_values) {
+    str = str.replace(/\{\$([^}]+)}/g, function(match, key) {
+      return (opt_values != null && key in opt_values) ? opt_values[key] :
+                                                         match;
+    });
+  }
+  return str;
+};
+
+
+/**
+ * Gets a localized message. If the message does not have a translation, gives a
+ * fallback message.
+ *
+ * This is useful when introducing a new message that has not yet been
+ * translated into all languages.
+ *
+ * This function is a compiler primitive. Must be used in the form:
+ * <code>var x = goog.getMsgWithFallback(MSG_A, MSG_B);</code>
+ * where MSG_A and MSG_B were initialized with goog.getMsg.
+ *
+ * @param {string} a The preferred message.
+ * @param {string} b The fallback message.
+ * @return {string} The best translated message.
+ */
+goog.getMsgWithFallback = function(a, b) {
+  return a;
+};
+
+
+/**
+ * Exposes an unobfuscated global namespace path for the given object.
+ * Note that fields of the exported object *will* be obfuscated, unless they are
+ * exported in turn via this function or goog.exportProperty.
+ *
+ * Also handy for making public items that are defined in anonymous closures.
+ *
+ * ex. goog.exportSymbol('public.path.Foo', Foo);
+ *
+ * ex. goog.exportSymbol('public.path.Foo.staticFunction', Foo.staticFunction);
+ *     public.path.Foo.staticFunction();
+ *
+ * ex. goog.exportSymbol('public.path.Foo.prototype.myMethod',
+ *                       Foo.prototype.myMethod);
+ *     new public.path.Foo().myMethod();
+ *
+ * @param {string} publicPath Unobfuscated name to export.
+ * @param {*} object Object the name should point to.
+ * @param {Object=} opt_objectToExportTo The object to add the path to; default
+ *     is goog.global.
+ */
+goog.exportSymbol = function(publicPath, object, opt_objectToExportTo) {
+  goog.exportPath_(publicPath, object, opt_objectToExportTo);
+};
+
+
+/**
+ * Exports a property unobfuscated into the object's namespace.
+ * ex. goog.exportProperty(Foo, 'staticFunction', Foo.staticFunction);
+ * ex. goog.exportProperty(Foo.prototype, 'myMethod', Foo.prototype.myMethod);
+ * @param {Object} object Object whose static property is being exported.
+ * @param {string} publicName Unobfuscated name to export.
+ * @param {*} symbol Object the name should point to.
+ */
+goog.exportProperty = function(object, publicName, symbol) {
+  object[publicName] = symbol;
+};
+
+
+/**
+ * Inherit the prototype methods from one constructor into another.
+ *
+ * Usage:
+ * <pre>
+ * function ParentClass(a, b) { }
+ * ParentClass.prototype.foo = function(a) { };
+ *
+ * function ChildClass(a, b, c) {
+ *   ChildClass.base(this, 'constructor', a, b);
+ * }
+ * goog.inherits(ChildClass, ParentClass);
+ *
+ * var child = new ChildClass('a', 'b', 'see');
+ * child.foo(); // This works.
+ * </pre>
+ *
+ * @param {!Function} childCtor Child class.
+ * @param {!Function} parentCtor Parent class.
+ */
+goog.inherits = function(childCtor, parentCtor) {
+  /** @constructor */
+  function tempCtor() {}
+  tempCtor.prototype = parentCtor.prototype;
+  childCtor.superClass_ = parentCtor.prototype;
+  childCtor.prototype = new tempCtor();
+  /** @override */
+  childCtor.prototype.constructor = childCtor;
+
+  /**
+   * Calls superclass constructor/method.
+   *
+   * This function is only available if you use goog.inherits to
+   * express inheritance relationships between classes.
+   *
+   * NOTE: This is a replacement for goog.base and for superClass_
+   * property defined in childCtor.
+   *
+   * @param {!Object} me Should always be "this".
+   * @param {string} methodName The method name to call. Calling
+   *     superclass constructor can be done with the special string
+   *     'constructor'.
+   * @param {...*} var_args The arguments to pass to superclass
+   *     method/constructor.
+   * @return {*} The return value of the superclass method/constructor.
+   */
+  childCtor.base = function(me, methodName, var_args) {
+    // Copying using loop to avoid deop due to passing arguments object to
+    // function. This is faster in many JS engines as of late 2014.
+    var args = new Array(arguments.length - 2);
+    for (var i = 2; i < arguments.length; i++) {
+      args[i - 2] = arguments[i];
+    }
+    return parentCtor.prototype[methodName].apply(me, args);
+  };
+};
+
+
+/**
+ * Call up to the superclass.
+ *
+ * If this is called from a constructor, then this calls the superclass
+ * constructor with arguments 1-N.
+ *
+ * If this is called from a prototype method, then you must pass the name of the
+ * method as the second argument to this function. If you do not, you will get a
+ * runtime error. This calls the superclass' method with arguments 2-N.
+ *
+ * This function only works if you use goog.inherits to express inheritance
+ * relationships between your classes.
+ *
+ * This function is a compiler primitive. At compile-time, the compiler will do
+ * macro expansion to remove a lot of the extra overhead that this function
+ * introduces. The compiler will also enforce a lot of the assumptions that this
+ * function makes, and treat it as a compiler error if you break them.
+ *
+ * @param {!Object} me Should always be "this".
+ * @param {*=} opt_methodName The method name if calling a super method.
+ * @param {...*} var_args The rest of the arguments.
+ * @return {*} The return value of the superclass method.
+ * @suppress {es5Strict} This method can not be used in strict mode, but
+ *     all Closure Library consumers must depend on this file.
+ * @deprecated goog.base is not strict mode compatible.  Prefer the static
+ *     "base" method added to the constructor by goog.inherits
+ *     or ES6 classes and the "super" keyword.
+ */
+goog.base = function(me, opt_methodName, var_args) {
+  var caller = arguments.callee.caller;
+
+  if (goog.STRICT_MODE_COMPATIBLE || (goog.DEBUG && !caller)) {
+    throw new Error(
+        'arguments.caller not defined.  goog.base() cannot be used ' +
+        'with strict mode code. See ' +
+        'http://www.ecma-international.org/ecma-262/5.1/#sec-C');
+  }
+
+  if (caller.superClass_) {
+    // Copying using loop to avoid deop due to passing arguments object to
+    // function. This is faster in many JS engines as of late 2014.
+    var ctorArgs = new Array(arguments.length - 1);
+    for (var i = 1; i < arguments.length; i++) {
+      ctorArgs[i - 1] = arguments[i];
+    }
+    // This is a constructor. Call the superclass constructor.
+    return caller.superClass_.constructor.apply(me, ctorArgs);
+  }
+
+  // Copying using loop to avoid deop due to passing arguments object to
+  // function. This is faster in many JS engines as of late 2014.
+  var args = new Array(arguments.length - 2);
+  for (var i = 2; i < arguments.length; i++) {
+    args[i - 2] = arguments[i];
+  }
+  var foundCaller = false;
+  for (var ctor = me.constructor; ctor;
+       ctor = ctor.superClass_ && ctor.superClass_.constructor) {
+    if (ctor.prototype[opt_methodName] === caller) {
+      foundCaller = true;
+    } else if (foundCaller) {
+      return ctor.prototype[opt_methodName].apply(me, args);
+    }
+  }
+
+  // If we did not find the caller in the prototype chain, then one of two
+  // things happened:
+  // 1) The caller is an instance method.
+  // 2) This method was not called by the right caller.
+  if (me[opt_methodName] === caller) {
+    return me.constructor.prototype[opt_methodName].apply(me, args);
+  } else {
+    throw new Error(
+        'goog.base called from a method of one name ' +
+        'to a method of a different name');
+  }
+};
+
+
+/**
+ * Allow for aliasing within scope functions.  This function exists for
+ * uncompiled code - in compiled code the calls will be inlined and the aliases
+ * applied.  In uncompiled code the function is simply run since the aliases as
+ * written are valid JavaScript.
+ *
+ * MOE:begin_intracomment_strip
+ * See the goog.scope document at http://go/goog.scope
+ * MOE:end_intracomment_strip
+ *
+ * @param {function()} fn Function to call.  This function can contain aliases
+ *     to namespaces (e.g. "var dom = goog.dom") or classes
+ *     (e.g. "var Timer = goog.Timer").
+ */
+goog.scope = function(fn) {
+  if (goog.isInModuleLoader_()) {
+    throw new Error('goog.scope is not supported within a goog.module.');
+  }
+  fn.call(goog.global);
+};
+
+
+/*
+ * To support uncompiled, strict mode bundles that use eval to divide source
+ * like so:
+ *    eval('someSource;//# sourceUrl sourcefile.js');
+ * We need to export the globally defined symbols "goog" and "COMPILED".
+ * Exporting "goog" breaks the compiler optimizations, so we required that
+ * be defined externally.
+ * NOTE: We don't use goog.exportSymbol here because we don't want to trigger
+ * extern generation when that compiler option is enabled.
+ */
+if (!COMPILED) {
+  goog.global['COMPILED'] = COMPILED;
+}
+
+
+//==============================================================================
+// goog.defineClass implementation
+//==============================================================================
+
+
+/**
+ * Creates a restricted form of a Closure "class":
+ *   - from the compiler's perspective, the instance returned from the
+ *     constructor is sealed (no new properties may be added).  This enables
+ *     better checks.
+ *   - the compiler will rewrite this definition to a form that is optimal
+ *     for type checking and optimization (initially this will be a more
+ *     traditional form).
+ *
+ * @param {Function} superClass The superclass, Object or null.
+ * @param {goog.defineClass.ClassDescriptor} def
+ *     An object literal describing
+ *     the class.  It may have the following properties:
+ *     "constructor": the constructor function
+ *     "statics": an object literal containing methods to add to the constructor
+ *        as "static" methods or a function that will receive the constructor
+ *        function as its only parameter to which static properties can
+ *        be added.
+ *     all other properties are added to the prototype.
+ * @return {!Function} The class constructor.
+ */
+goog.defineClass = function(superClass, def) {
+  // TODO(johnlenz): consider making the superClass an optional parameter.
+  var constructor = def.constructor;
+  var statics = def.statics;
+  // Wrap the constructor prior to setting up the prototype and static methods.
+  if (!constructor || constructor == Object.prototype.constructor) {
+    constructor = function() {
+      throw new Error(
+          'cannot instantiate an interface (no constructor defined).');
+    };
+  }
+
+  var cls = goog.defineClass.createSealingConstructor_(constructor, superClass);
+  if (superClass) {
+    goog.inherits(cls, superClass);
+  }
+
+  // Remove all the properties that should not be copied to the prototype.
+  delete def.constructor;
+  delete def.statics;
+
+  goog.defineClass.applyProperties_(cls.prototype, def);
+  if (statics != null) {
+    if (statics instanceof Function) {
+      statics(cls);
+    } else {
+      goog.defineClass.applyProperties_(cls, statics);
+    }
+  }
+
+  return cls;
+};
+
+
+/**
+ * @typedef {{
+ *   constructor: (!Function|undefined),
+ *   statics: (Object|undefined|function(Function):void)
+ * }}
+ */
+goog.defineClass.ClassDescriptor;
+
+
+/**
+ * @define {boolean} Whether the instances returned by goog.defineClass should
+ *     be sealed when possible.
+ *
+ * When sealing is disabled the constructor function will not be wrapped by
+ * goog.defineClass, making it incompatible with ES6 class methods.
+ */
+goog.define('goog.defineClass.SEAL_CLASS_INSTANCES', goog.DEBUG);
+
+
+/**
+ * If goog.defineClass.SEAL_CLASS_INSTANCES is enabled and Object.seal is
+ * defined, this function will wrap the constructor in a function that seals the
+ * results of the provided constructor function.
+ *
+ * @param {!Function} ctr The constructor whose results maybe be sealed.
+ * @param {Function} superClass The superclass constructor.
+ * @return {!Function} The replacement constructor.
+ * @private
+ */
+goog.defineClass.createSealingConstructor_ = function(ctr, superClass) {
+  if (!goog.defineClass.SEAL_CLASS_INSTANCES) {
+    // Do now wrap the constructor when sealing is disabled. Angular code
+    // depends on this for injection to work properly.
+    return ctr;
+  }
+
+  // Compute whether the constructor is sealable at definition time, rather
+  // than when the instance is being constructed.
+  var superclassSealable = !goog.defineClass.isUnsealable_(superClass);
+
+  /**
+   * @this {Object}
+   * @return {?}
+   */
+  var wrappedCtr = function() {
+    // Don't seal an instance of a subclass when it calls the constructor of
+    // its super class as there is most likely still setup to do.
+    var instance = ctr.apply(this, arguments) || this;
+    instance[goog.UID_PROPERTY_] = instance[goog.UID_PROPERTY_];
+
+    if (this.constructor === wrappedCtr && superclassSealable &&
+        Object.seal instanceof Function) {
+      Object.seal(instance);
+    }
+    return instance;
+  };
+
+  return wrappedCtr;
+};
+
+
+/**
+ * @param {Function} ctr The constructor to test.
+ * @return {boolean} Whether the constructor has been tagged as unsealable
+ *     using goog.tagUnsealableClass.
+ * @private
+ */
+goog.defineClass.isUnsealable_ = function(ctr) {
+  return ctr && ctr.prototype &&
+      ctr.prototype[goog.UNSEALABLE_CONSTRUCTOR_PROPERTY_];
+};
+
+
+// TODO(johnlenz): share these values with the goog.object
+/**
+ * The names of the fields that are defined on Object.prototype.
+ * @type {!Array<string>}
+ * @private
+ * @const
+ */
+goog.defineClass.OBJECT_PROTOTYPE_FIELDS_ = [
+  'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable',
+  'toLocaleString', 'toString', 'valueOf'
+];
+
+
+// TODO(johnlenz): share this function with the goog.object
+/**
+ * @param {!Object} target The object to add properties to.
+ * @param {!Object} source The object to copy properties from.
+ * @private
+ */
+goog.defineClass.applyProperties_ = function(target, source) {
+  // TODO(johnlenz): update this to support ES5 getters/setters
+
+  var key;
+  for (key in source) {
+    if (Object.prototype.hasOwnProperty.call(source, key)) {
+      target[key] = source[key];
+    }
+  }
+
+  // For IE the for-in-loop does not contain any properties that are not
+  // enumerable on the prototype object (for example isPrototypeOf from
+  // Object.prototype) and it will also not include 'replace' on objects that
+  // extend String and change 'replace' (not that it is common for anyone to
+  // extend anything except Object).
+  for (var i = 0; i < goog.defineClass.OBJECT_PROTOTYPE_FIELDS_.length; i++) {
+    key = goog.defineClass.OBJECT_PROTOTYPE_FIELDS_[i];
+    if (Object.prototype.hasOwnProperty.call(source, key)) {
+      target[key] = source[key];
+    }
+  }
+};
+
+
+/**
+ * Sealing classes breaks the older idiom of assigning properties on the
+ * prototype rather than in the constructor. As such, goog.defineClass
+ * must not seal subclasses of these old-style classes until they are fixed.
+ * Until then, this marks a class as "broken", instructing defineClass
+ * not to seal subclasses.
+ * @param {!Function} ctr The legacy constructor to tag as unsealable.
+ */
+goog.tagUnsealableClass = function(ctr) {
+  if (!COMPILED && goog.defineClass.SEAL_CLASS_INSTANCES) {
+    ctr.prototype[goog.UNSEALABLE_CONSTRUCTOR_PROPERTY_] = true;
+  }
+};
+
+
+/**
+ * Name for unsealable tag property.
+ * @const @private {string}
+ */
+goog.UNSEALABLE_CONSTRUCTOR_PROPERTY_ = 'goog_defineClass_legacy_unsealable';
+
+
+/**
+ * Returns a newly created map from language mode string to a boolean
+ * indicating whether transpilation should be done for that mode.
+ *
+ * Guaranteed invariant:
+ * For any two modes, l1 and l2 where l2 is a newer mode than l1,
+ * `map[l1] == true` implies that `map[l2] == true`.
+ * @private
+ * @return {!Object<string, boolean>}
+ */
+goog.createRequiresTranspilation_ = function() {
+  var /** !Object<string, boolean> */ requiresTranspilation = {'es3': false};
+  var transpilationRequiredForAllLaterModes = false;
+
+  /**
+   * Adds an entry to requiresTranspliation for the given language mode.
+   *
+   * IMPORTANT: Calls must be made in order from oldest to newest language
+   * mode.
+   * @param {string} modeName
+   * @param {function(): boolean} isSupported Returns true if the JS engine
+   *     supports the given mode.
+   */
+  function addNewerLanguageTranspilationCheck(modeName, isSupported) {
+    if (transpilationRequiredForAllLaterModes) {
+      requiresTranspilation[modeName] = true;
+    } else if (isSupported()) {
+      requiresTranspilation[modeName] = false;
+    } else {
+      requiresTranspilation[modeName] = true;
+      transpilationRequiredForAllLaterModes = true;
+    }
+  }
+
+  /**
+   * Does the given code evaluate without syntax errors and return a truthy
+   * result?
+   */
+  function /** boolean */ evalCheck(/** string */ code) {
+    try {
+      return !!eval(code);
+    } catch (ignored) {
+      return false;
+    }
+  }
+
+  var userAgent = goog.global.navigator && goog.global.navigator.userAgent ?
+      goog.global.navigator.userAgent :
+      '';
+
+  // Identify ES3-only browsers by their incorrect treatment of commas.
+  addNewerLanguageTranspilationCheck('es5', function() {
+    return evalCheck('[1,].length==1');
+  });
+  addNewerLanguageTranspilationCheck('es6', function() {
+    // Edge has a non-deterministic (i.e., not reproducible) bug with ES6:
+    // https://github.com/Microsoft/ChakraCore/issues/1496.
+    // MOE:begin_strip
+    // TODO(joeltine): Our internal web-testing version of Edge will need to be
+    // updated before we can remove this check. See http://b/34945376.
+    // MOE:end_strip
+    var re = /Edge\/(\d+)(\.\d)*/i;
+    var edgeUserAgent = userAgent.match(re);
+    if (edgeUserAgent && Number(edgeUserAgent[1]) < 15) {
+      return false;
+    }
+    // Test es6: [FF50 (?), Edge 14 (?), Chrome 50]
+    //   (a) default params (specifically shadowing locals),
+    //   (b) destructuring, (c) block-scoped functions,
+    //   (d) for-of (const), (e) new.target/Reflect.construct
+    var es6fullTest =
+        'class X{constructor(){if(new.target!=String)throw 1;this.x=42}}' +
+        'let q=Reflect.construct(X,[],String);if(q.x!=42||!(q instanceof ' +
+        'String))throw 1;for(const a of[2,3]){if(a==2)continue;function ' +
+        'f(z={a}){let a=0;return z.a}{function f(){return 0;}}return f()' +
+        '==3}';
+
+    return evalCheck('(()=>{"use strict";' + es6fullTest + '})()');
+  });
+  // TODO(joeltine): Remove es6-impl references for b/31340605.
+  // Consider es6-impl (widely-implemented es6 features) to be supported
+  // whenever es6 is supported. Technically es6-impl is a lower level of
+  // support than es6, but we don't have tests specifically for it.
+  addNewerLanguageTranspilationCheck('es6-impl', function() {
+    return true;
+  });
+  // ** and **= are the only new features in 'es7'
+  addNewerLanguageTranspilationCheck('es7', function() {
+    return evalCheck('2 ** 2 == 4');
+  });
+  // async functions are the only new features in 'es8'
+  addNewerLanguageTranspilationCheck('es8', function() {
+    return evalCheck('async () => 1, true');
+  });
+  return requiresTranspilation;
+};
diff --git a/third_party/ink/closure/crypt/base64.js b/third_party/ink/closure/crypt/base64.js
new file mode 100644
index 0000000..026aa956
--- /dev/null
+++ b/third_party/ink/closure/crypt/base64.js
@@ -0,0 +1,370 @@
+// Copyright 2007 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 Base64 en/decoding. Not much to say here except that we
+ * work with decoded values in arrays of bytes. By "byte" I mean a number
+ * in [0, 255].
+ *
+ * @author doughtie@google.com (Gavin Doughtie)
+ * @author fschneider@google.com (Fritz Schneider)
+ */
+
+goog.provide('goog.crypt.base64');
+
+goog.require('goog.asserts');
+goog.require('goog.crypt');
+goog.require('goog.string');
+goog.require('goog.userAgent');
+goog.require('goog.userAgent.product');
+
+// Static lookup maps, lazily populated by init_()
+
+
+/**
+ * Maps bytes to characters.
+ * @type {Object}
+ * @private
+ */
+goog.crypt.base64.byteToCharMap_ = null;
+
+
+/**
+ * Maps characters to bytes. Used for normal and websafe characters.
+ * @type {Object}
+ * @private
+ */
+goog.crypt.base64.charToByteMap_ = null;
+
+
+/**
+ * Maps bytes to websafe characters.
+ * @type {Object}
+ * @private
+ */
+goog.crypt.base64.byteToCharMapWebSafe_ = null;
+
+
+/**
+ * Our default alphabet, shared between
+ * ENCODED_VALS and ENCODED_VALS_WEBSAFE
+ * @type {string}
+ */
+goog.crypt.base64.ENCODED_VALS_BASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
+    'abcdefghijklmnopqrstuvwxyz' +
+    '0123456789';
+
+
+/**
+ * Our default alphabet. Value 64 (=) is special; it means "nothing."
+ * @type {string}
+ */
+goog.crypt.base64.ENCODED_VALS = goog.crypt.base64.ENCODED_VALS_BASE + '+/=';
+
+
+/**
+ * Our websafe alphabet.
+ * @type {string}
+ */
+goog.crypt.base64.ENCODED_VALS_WEBSAFE =
+    goog.crypt.base64.ENCODED_VALS_BASE + '-_.';
+
+
+/**
+ * White list of implementations with known-good native atob and btoa functions.
+ * Listing these explicitly (via the ASSUME_* wrappers) benefits dead-code
+ * removal in per-browser compilations.
+ * @private {boolean}
+ */
+goog.crypt.base64.ASSUME_NATIVE_SUPPORT_ = goog.userAgent.GECKO ||
+    (goog.userAgent.WEBKIT && !goog.userAgent.product.SAFARI) ||
+    goog.userAgent.OPERA;
+
+
+/**
+ * Does this browser have a working btoa function?
+ * @private {boolean}
+ */
+goog.crypt.base64.HAS_NATIVE_ENCODE_ =
+    goog.crypt.base64.ASSUME_NATIVE_SUPPORT_ ||
+    typeof(goog.global.btoa) == 'function';
+
+
+/**
+ * Does this browser have a working atob function?
+ * We blacklist known-bad implementations:
+ *  - IE (10+) added atob() but it does not tolerate whitespace on the input.
+ * @private {boolean}
+ */
+goog.crypt.base64.HAS_NATIVE_DECODE_ =
+    goog.crypt.base64.ASSUME_NATIVE_SUPPORT_ ||
+    (!goog.userAgent.product.SAFARI && !goog.userAgent.IE &&
+     typeof(goog.global.atob) == 'function');
+
+
+/**
+ * Base64-encode an array of bytes.
+ *
+ * @param {Array<number>|Uint8Array} input An array of bytes (numbers with
+ *     value in [0, 255]) to encode.
+ * @param {boolean=} opt_webSafe True indicates we should use the alternative
+ *     alphabet, which does not require escaping for use in URLs.
+ * @return {string} The base64 encoded string.
+ */
+goog.crypt.base64.encodeByteArray = function(input, opt_webSafe) {
+  // Assert avoids runtime dependency on goog.isArrayLike, which helps reduce
+  // size of jscompiler output, and which yields slight performance increase.
+  goog.asserts.assert(
+      goog.isArrayLike(input), 'encodeByteArray takes an array as a parameter');
+
+  goog.crypt.base64.init_();
+
+  var byteToCharMap = opt_webSafe ? goog.crypt.base64.byteToCharMapWebSafe_ :
+                                    goog.crypt.base64.byteToCharMap_;
+
+  var output = [];
+
+  for (var i = 0; i < input.length; i += 3) {
+    var byte1 = input[i];
+    var haveByte2 = i + 1 < input.length;
+    var byte2 = haveByte2 ? input[i + 1] : 0;
+    var haveByte3 = i + 2 < input.length;
+    var byte3 = haveByte3 ? input[i + 2] : 0;
+
+    var outByte1 = byte1 >> 2;
+    var outByte2 = ((byte1 & 0x03) << 4) | (byte2 >> 4);
+    var outByte3 = ((byte2 & 0x0F) << 2) | (byte3 >> 6);
+    var outByte4 = byte3 & 0x3F;
+
+    if (!haveByte3) {
+      outByte4 = 64;
+
+      if (!haveByte2) {
+        outByte3 = 64;
+      }
+    }
+
+    output.push(
+        byteToCharMap[outByte1], byteToCharMap[outByte2],
+        byteToCharMap[outByte3], byteToCharMap[outByte4]);
+  }
+
+  return output.join('');
+};
+
+
+/**
+ * Base64-encode a string.
+ *
+ * @param {string} input A string to encode.
+ * @param {boolean=} opt_webSafe True indicates we should use the alternative
+ *     alphabet, which does not require escaping for use in URLs.
+ * @return {string} The base64 encoded string.
+ */
+goog.crypt.base64.encodeString = function(input, opt_webSafe) {
+  // Shortcut for browsers that implement
+  // a native base64 encoder in the form of "btoa/atob"
+  if (goog.crypt.base64.HAS_NATIVE_ENCODE_ && !opt_webSafe) {
+    return goog.global.btoa(input);
+  }
+  return goog.crypt.base64.encodeByteArray(
+      goog.crypt.stringToByteArray(input), opt_webSafe);
+};
+
+
+/**
+ * Base64-decode a string.
+ *
+ * @param {string} input Input to decode. Any whitespace is ignored, and the
+ *     input maybe encoded with either supported alphabet (or a mix thereof).
+ * @param {boolean=} opt_webSafe True indicates we should use the alternative
+ *     alphabet, which does not require escaping for use in URLs. Note that
+ *     passing false may also still allow webSafe input decoding, when the
+ *     fallback decoder is used on browsers without native support.
+ * @return {string} string representing the decoded value.
+ */
+goog.crypt.base64.decodeString = function(input, opt_webSafe) {
+  // Shortcut for browsers that implement
+  // a native base64 encoder in the form of "btoa/atob"
+  if (goog.crypt.base64.HAS_NATIVE_DECODE_ && !opt_webSafe) {
+    return goog.global.atob(input);
+  }
+  var output = '';
+  function pushByte(b) { output += String.fromCharCode(b); }
+
+  goog.crypt.base64.decodeStringInternal_(input, pushByte);
+
+  return output;
+};
+
+
+/**
+ * Base64-decode a string to an Array of numbers.
+ *
+ * In base-64 decoding, groups of four characters are converted into three
+ * bytes.  If the encoder did not apply padding, the input length may not
+ * be a multiple of 4.
+ *
+ * In this case, the last group will have fewer than 4 characters, and
+ * padding will be inferred.  If the group has one or two characters, it decodes
+ * to one byte.  If the group has three characters, it decodes to two bytes.
+ *
+ * @param {string} input Input to decode. Any whitespace is ignored, and the
+ *     input maybe encoded with either supported alphabet (or a mix thereof).
+ * @param {boolean=} opt_ignored Unused parameter, retained for compatibility.
+ * @return {!Array<number>} bytes representing the decoded value.
+ */
+goog.crypt.base64.decodeStringToByteArray = function(input, opt_ignored) {
+  var output = [];
+  function pushByte(b) { output.push(b); }
+
+  goog.crypt.base64.decodeStringInternal_(input, pushByte);
+
+  return output;
+};
+
+
+/**
+ * Base64-decode a string to a Uint8Array.
+ *
+ * Note that Uint8Array is not supported on older browsers, e.g. IE < 10.
+ * @see http://caniuse.com/uint8array
+ *
+ * In base-64 decoding, groups of four characters are converted into three
+ * bytes.  If the encoder did not apply padding, the input length may not
+ * be a multiple of 4.
+ *
+ * In this case, the last group will have fewer than 4 characters, and
+ * padding will be inferred.  If the group has one or two characters, it decodes
+ * to one byte.  If the group has three characters, it decodes to two bytes.
+ *
+ * @param {string} input Input to decode. Any whitespace is ignored, and the
+ *     input maybe encoded with either supported alphabet (or a mix thereof).
+ * @return {!Uint8Array} bytes representing the decoded value.
+ */
+goog.crypt.base64.decodeStringToUint8Array = function(input) {
+  goog.asserts.assert(
+      !goog.userAgent.IE || goog.userAgent.isVersionOrHigher('10'),
+      'Browser does not support typed arrays');
+  var len = input.length;
+  // Check if there are trailing '=' as padding in the b64 string.
+  var placeholders = 0;
+  if (input[len - 2] === '=') {
+    placeholders = 2;
+  } else if (input[len - 1] === '=') {
+    placeholders = 1;
+  }
+  var output = new Uint8Array(Math.ceil(len * 3 / 4) - placeholders);
+  var outLen = 0;
+  function pushByte(b) {
+    output[outLen++] = b;
+  }
+
+  goog.crypt.base64.decodeStringInternal_(input, pushByte);
+
+  return output.subarray(0, outLen);
+};
+
+
+/**
+ * @param {string} input Input to decode.
+ * @param {function(number):void} pushByte result accumulator.
+ * @private
+ */
+goog.crypt.base64.decodeStringInternal_ = function(input, pushByte) {
+  goog.crypt.base64.init_();
+
+  var nextCharIndex = 0;
+  /**
+   * @param {number} default_val Used for end-of-input.
+   * @return {number} The next 6-bit value, or the default for end-of-input.
+   */
+  function getByte(default_val) {
+    while (nextCharIndex < input.length) {
+      var ch = input.charAt(nextCharIndex++);
+      var b = goog.crypt.base64.charToByteMap_[ch];
+      if (b != null) {
+        return b;  // Common case: decoded the char.
+      }
+      if (!goog.string.isEmptyOrWhitespace(ch)) {
+        throw new Error('Unknown base64 encoding at char: ' + ch);
+      }
+      // We encountered whitespace: loop around to the next input char.
+    }
+    return default_val;  // No more input remaining.
+  }
+
+  while (true) {
+    var byte1 = getByte(-1);
+    var byte2 = getByte(0);
+    var byte3 = getByte(64);
+    var byte4 = getByte(64);
+
+    // The common case is that all four bytes are present, so if we have byte4
+    // we can skip over the truncated input special case handling.
+    if (byte4 === 64) {
+      if (byte1 === -1) {
+        return;  // Terminal case: no input left to decode.
+      }
+      // Here we know an intermediate number of bytes are missing.
+      // The defaults for byte2, byte3 and byte4 apply the inferred padding
+      // rules per the public API documentation. i.e: 1 byte
+      // missing should yield 2 bytes of output, but 2 or 3 missing bytes yield
+      // a single byte of output. (Recall that 64 corresponds the padding char).
+    }
+
+    var outByte1 = (byte1 << 2) | (byte2 >> 4);
+    pushByte(outByte1);
+
+    if (byte3 != 64) {
+      var outByte2 = ((byte2 << 4) & 0xF0) | (byte3 >> 2);
+      pushByte(outByte2);
+
+      if (byte4 != 64) {
+        var outByte3 = ((byte3 << 6) & 0xC0) | byte4;
+        pushByte(outByte3);
+      }
+    }
+  }
+};
+
+
+/**
+ * Lazy static initialization function. Called before
+ * accessing any of the static map variables.
+ * @private
+ */
+goog.crypt.base64.init_ = function() {
+  if (!goog.crypt.base64.byteToCharMap_) {
+    goog.crypt.base64.byteToCharMap_ = {};
+    goog.crypt.base64.charToByteMap_ = {};
+    goog.crypt.base64.byteToCharMapWebSafe_ = {};
+
+    // We want quick mappings back and forth, so we precompute two maps.
+    for (var i = 0; i < goog.crypt.base64.ENCODED_VALS.length; i++) {
+      goog.crypt.base64.byteToCharMap_[i] =
+          goog.crypt.base64.ENCODED_VALS.charAt(i);
+      goog.crypt.base64.charToByteMap_[goog.crypt.base64.byteToCharMap_[i]] = i;
+      goog.crypt.base64.byteToCharMapWebSafe_[i] =
+          goog.crypt.base64.ENCODED_VALS_WEBSAFE.charAt(i);
+
+      // Be forgiving when decoding and correctly decode both encodings.
+      if (i >= goog.crypt.base64.ENCODED_VALS_BASE.length) {
+        goog.crypt.base64
+            .charToByteMap_[goog.crypt.base64.ENCODED_VALS_WEBSAFE.charAt(i)] =
+            i;
+      }
+    }
+  }
+};
diff --git a/third_party/ink/closure/crypt/crypt.js b/third_party/ink/closure/crypt/crypt.js
new file mode 100644
index 0000000..a0e4f02
--- /dev/null
+++ b/third_party/ink/closure/crypt/crypt.js
@@ -0,0 +1,196 @@
+// Copyright 2008 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 Namespace with crypto related helper functions.
+ * @author pupius@google.com (Daniel Pupius)
+ */
+
+goog.provide('goog.crypt');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+
+
+/**
+ * Turns a string into an array of bytes; a "byte" being a JS number in the
+ * range 0-255. Multi-byte characters are written as little-endian.
+ * @param {string} str String value to arrify.
+ * @return {!Array<number>} Array of numbers corresponding to the
+ *     UCS character codes of each character in str.
+ */
+goog.crypt.stringToByteArray = function(str) {
+  var output = [], p = 0;
+  for (var i = 0; i < str.length; i++) {
+    var c = str.charCodeAt(i);
+    // NOTE: c <= 0xffff since JavaScript strings are UTF-16.
+    if (c > 0xff) {
+      output[p++] = c & 0xff;
+      c >>= 8;
+    }
+    output[p++] = c;
+  }
+  return output;
+};
+
+
+/**
+ * Turns an array of numbers into the string given by the concatenation of the
+ * characters to which the numbers correspond.
+ * @param {!Uint8Array|!Array<number>} bytes Array of numbers representing
+ *     characters.
+ * @return {string} Stringification of the array.
+ */
+goog.crypt.byteArrayToString = function(bytes) {
+  var CHUNK_SIZE = 8192;
+
+  // Special-case the simple case for speed's sake.
+  if (bytes.length <= CHUNK_SIZE) {
+    return String.fromCharCode.apply(null, bytes);
+  }
+
+  // The remaining logic splits conversion by chunks since
+  // Function#apply() has a maximum parameter count.
+  // See discussion: http://goo.gl/LrWmZ9
+
+  var str = '';
+  for (var i = 0; i < bytes.length; i += CHUNK_SIZE) {
+    var chunk = goog.array.slice(bytes, i, i + CHUNK_SIZE);
+    str += String.fromCharCode.apply(null, chunk);
+  }
+  return str;
+};
+
+
+/**
+ * Turns an array of numbers into the hex string given by the concatenation of
+ * the hex values to which the numbers correspond.
+ * @param {Uint8Array|Array<number>} array Array of numbers representing
+ *     characters.
+ * @return {string} Hex string.
+ */
+goog.crypt.byteArrayToHex = function(array) {
+  return goog.array
+      .map(
+          array,
+          function(numByte) {
+            var hexByte = numByte.toString(16);
+            return hexByte.length > 1 ? hexByte : '0' + hexByte;
+          })
+      .join('');
+};
+
+
+/**
+ * Converts a hex string into an integer array.
+ * @param {string} hexString Hex string of 16-bit integers (two characters
+ *     per integer).
+ * @return {!Array<number>} Array of {0,255} integers for the given string.
+ */
+goog.crypt.hexToByteArray = function(hexString) {
+  goog.asserts.assert(
+      hexString.length % 2 == 0, 'Key string length must be multiple of 2');
+  var arr = [];
+  for (var i = 0; i < hexString.length; i += 2) {
+    arr.push(parseInt(hexString.substring(i, i + 2), 16));
+  }
+  return arr;
+};
+
+
+/**
+ * Converts a JS string to a UTF-8 "byte" array.
+ * @param {string} str 16-bit unicode string.
+ * @return {!Array<number>} UTF-8 byte array.
+ */
+goog.crypt.stringToUtf8ByteArray = function(str) {
+  // TODO(pupius): Use native implementations if/when available
+  var out = [], p = 0;
+  for (var i = 0; i < str.length; i++) {
+    var c = str.charCodeAt(i);
+    if (c < 128) {
+      out[p++] = c;
+    } else if (c < 2048) {
+      out[p++] = (c >> 6) | 192;
+      out[p++] = (c & 63) | 128;
+    } else if (
+        ((c & 0xFC00) == 0xD800) && (i + 1) < str.length &&
+        ((str.charCodeAt(i + 1) & 0xFC00) == 0xDC00)) {
+      // Surrogate Pair
+      c = 0x10000 + ((c & 0x03FF) << 10) + (str.charCodeAt(++i) & 0x03FF);
+      out[p++] = (c >> 18) | 240;
+      out[p++] = ((c >> 12) & 63) | 128;
+      out[p++] = ((c >> 6) & 63) | 128;
+      out[p++] = (c & 63) | 128;
+    } else {
+      out[p++] = (c >> 12) | 224;
+      out[p++] = ((c >> 6) & 63) | 128;
+      out[p++] = (c & 63) | 128;
+    }
+  }
+  return out;
+};
+
+
+/**
+ * Converts a UTF-8 byte array to JavaScript's 16-bit Unicode.
+ * @param {Uint8Array|Array<number>} bytes UTF-8 byte array.
+ * @return {string} 16-bit Unicode string.
+ */
+goog.crypt.utf8ByteArrayToString = function(bytes) {
+  // TODO(pupius): Use native implementations if/when available
+  var out = [], pos = 0, c = 0;
+  while (pos < bytes.length) {
+    var c1 = bytes[pos++];
+    if (c1 < 128) {
+      out[c++] = String.fromCharCode(c1);
+    } else if (c1 > 191 && c1 < 224) {
+      var c2 = bytes[pos++];
+      out[c++] = String.fromCharCode((c1 & 31) << 6 | c2 & 63);
+    } else if (c1 > 239 && c1 < 365) {
+      // Surrogate Pair
+      var c2 = bytes[pos++];
+      var c3 = bytes[pos++];
+      var c4 = bytes[pos++];
+      var u = ((c1 & 7) << 18 | (c2 & 63) << 12 | (c3 & 63) << 6 | c4 & 63) -
+          0x10000;
+      out[c++] = String.fromCharCode(0xD800 + (u >> 10));
+      out[c++] = String.fromCharCode(0xDC00 + (u & 1023));
+    } else {
+      var c2 = bytes[pos++];
+      var c3 = bytes[pos++];
+      out[c++] =
+          String.fromCharCode((c1 & 15) << 12 | (c2 & 63) << 6 | c3 & 63);
+    }
+  }
+  return out.join('');
+};
+
+
+/**
+ * XOR two byte arrays.
+ * @param {!Uint8Array|!Int8Array|!Array<number>} bytes1 Byte array 1.
+ * @param {!Uint8Array|!Int8Array|!Array<number>} bytes2 Byte array 2.
+ * @return {!Array<number>} Resulting XOR of the two byte arrays.
+ */
+goog.crypt.xorByteArray = function(bytes1, bytes2) {
+  goog.asserts.assert(
+      bytes1.length == bytes2.length, 'XOR array lengths must match');
+
+  var result = [];
+  for (var i = 0; i < bytes1.length; i++) {
+    result.push(bytes1[i] ^ bytes2[i]);
+  }
+  return result;
+};
diff --git a/third_party/ink/closure/debug/debug.js b/third_party/ink/closure/debug/debug.js
new file mode 100644
index 0000000..92d1945
--- /dev/null
+++ b/third_party/ink/closure/debug/debug.js
@@ -0,0 +1,666 @@
+// Copyright 2006 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 Logging and debugging utilities.
+ *
+ * @author pupius@google.com (Daniel Pupius)
+ * @see ../demos/debug.html
+ */
+
+goog.provide('goog.debug');
+
+goog.require('goog.array');
+goog.require('goog.debug.errorcontext');
+goog.require('goog.userAgent');
+
+
+/** @define {boolean} Whether logging should be enabled. */
+goog.define('goog.debug.LOGGING_ENABLED', goog.DEBUG);
+
+
+/** @define {boolean} Whether to force "sloppy" stack building. */
+goog.define('goog.debug.FORCE_SLOPPY_STACKS', false);
+
+
+/**
+ * Catches onerror events fired by windows and similar objects.
+ * @param {function(Object)} logFunc The function to call with the error
+ *    information.
+ * @param {boolean=} opt_cancel Whether to stop the error from reaching the
+ *    browser.
+ * @param {Object=} opt_target Object that fires onerror events.
+ */
+goog.debug.catchErrors = function(logFunc, opt_cancel, opt_target) {
+  var target = opt_target || goog.global;
+  var oldErrorHandler = target.onerror;
+  var retVal = !!opt_cancel;
+
+  // Chrome interprets onerror return value backwards (http://crbug.com/92062)
+  // until it was fixed in webkit revision r94061 (Webkit 535.3). This
+  // workaround still needs to be skipped in Safari after the webkit change
+  // gets pushed out in Safari.
+  // See https://bugs.webkit.org/show_bug.cgi?id=67119
+  if (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('535.3')) {
+    retVal = !retVal;
+  }
+
+  /**
+   * New onerror handler for this target. This onerror handler follows the spec
+   * according to
+   * http://www.whatwg.org/specs/web-apps/current-work/#runtime-script-errors
+   * The spec was changed in August 2013 to support receiving column information
+   * and an error object for all scripts on the same origin or cross origin
+   * scripts with the proper headers. See
+   * https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror
+   *
+   * @param {string} message The error message. For cross-origin errors, this
+   *     will be scrubbed to just "Script error.". For new browsers that have
+   *     updated to follow the latest spec, errors that come from origins that
+   *     have proper cross origin headers will not be scrubbed.
+   * @param {string} url The URL of the script that caused the error. The URL
+   *     will be scrubbed to "" for cross origin scripts unless the script has
+   *     proper cross origin headers and the browser has updated to the latest
+   *     spec.
+   * @param {number} line The line number in the script that the error
+   *     occurred on.
+   * @param {number=} opt_col The optional column number that the error
+   *     occurred on. Only browsers that have updated to the latest spec will
+   *     include this.
+   * @param {Error=} opt_error The optional actual error object for this
+   *     error that should include the stack. Only browsers that have updated
+   *     to the latest spec will inlude this parameter.
+   * @return {boolean} Whether to prevent the error from reaching the browser.
+   */
+  target.onerror = function(message, url, line, opt_col, opt_error) {
+    if (oldErrorHandler) {
+      oldErrorHandler(message, url, line, opt_col, opt_error);
+    }
+    logFunc({
+      message: message,
+      fileName: url,
+      line: line,
+      lineNumber: line,
+      col: opt_col,
+      error: opt_error
+    });
+    return retVal;
+  };
+};
+
+
+/**
+ * Creates a string representing an object and all its properties.
+ * @param {Object|null|undefined} obj Object to expose.
+ * @param {boolean=} opt_showFn Show the functions as well as the properties,
+ *     default is false.
+ * @return {string} The string representation of {@code obj}.
+ */
+goog.debug.expose = function(obj, opt_showFn) {
+  if (typeof obj == 'undefined') {
+    return 'undefined';
+  }
+  if (obj == null) {
+    return 'NULL';
+  }
+  var str = [];
+
+  for (var x in obj) {
+    if (!opt_showFn && goog.isFunction(obj[x])) {
+      continue;
+    }
+    var s = x + ' = ';
+
+    try {
+      s += obj[x];
+    } catch (e) {
+      s += '*** ' + e + ' ***';
+    }
+    str.push(s);
+  }
+  return str.join('\n');
+};
+
+
+/**
+ * Creates a string representing a given primitive or object, and for an
+ * object, all its properties and nested objects. NOTE: The output will include
+ * Uids on all objects that were exposed. Any added Uids will be removed before
+ * returning.
+ * @param {*} obj Object to expose.
+ * @param {boolean=} opt_showFn Also show properties that are functions (by
+ *     default, functions are omitted).
+ * @return {string} A string representation of {@code obj}.
+ */
+goog.debug.deepExpose = function(obj, opt_showFn) {
+  var str = [];
+
+  // Track any objects where deepExpose added a Uid, so they can be cleaned up
+  // before return. We do this globally, rather than only on ancestors so that
+  // if the same object appears in the output, you can see it.
+  var uidsToCleanup = [];
+  var ancestorUids = {};
+
+  var helper = function(obj, space) {
+    var nestspace = space + '  ';
+
+    var indentMultiline = function(str) {
+      return str.replace(/\n/g, '\n' + space);
+    };
+
+
+    try {
+      if (!goog.isDef(obj)) {
+        str.push('undefined');
+      } else if (goog.isNull(obj)) {
+        str.push('NULL');
+      } else if (goog.isString(obj)) {
+        str.push('"' + indentMultiline(obj) + '"');
+      } else if (goog.isFunction(obj)) {
+        str.push(indentMultiline(String(obj)));
+      } else if (goog.isObject(obj)) {
+        // Add a Uid if needed. The struct calls implicitly adds them.
+        if (!goog.hasUid(obj)) {
+          uidsToCleanup.push(obj);
+        }
+        var uid = goog.getUid(obj);
+        if (ancestorUids[uid]) {
+          str.push('*** reference loop detected (id=' + uid + ') ***');
+        } else {
+          ancestorUids[uid] = true;
+          str.push('{');
+          for (var x in obj) {
+            if (!opt_showFn && goog.isFunction(obj[x])) {
+              continue;
+            }
+            str.push('\n');
+            str.push(nestspace);
+            str.push(x + ' = ');
+            helper(obj[x], nestspace);
+          }
+          str.push('\n' + space + '}');
+          delete ancestorUids[uid];
+        }
+      } else {
+        str.push(obj);
+      }
+    } catch (e) {
+      str.push('*** ' + e + ' ***');
+    }
+  };
+
+  helper(obj, '');
+
+  // Cleanup any Uids that were added by the deepExpose.
+  for (var i = 0; i < uidsToCleanup.length; i++) {
+    goog.removeUid(uidsToCleanup[i]);
+  }
+
+  return str.join('');
+};
+
+
+/**
+ * Recursively outputs a nested array as a string.
+ * @param {Array<?>} arr The array.
+ * @return {string} String representing nested array.
+ */
+goog.debug.exposeArray = function(arr) {
+  var str = [];
+  for (var i = 0; i < arr.length; i++) {
+    if (goog.isArray(arr[i])) {
+      str.push(goog.debug.exposeArray(arr[i]));
+    } else {
+      str.push(arr[i]);
+    }
+  }
+  return '[ ' + str.join(', ') + ' ]';
+};
+
+
+/**
+ * Normalizes the error/exception object between browsers.
+ * @param {*} err Raw error object.
+ * @return {!{
+ *    message: (?|undefined),
+ *    name: (?|undefined),
+ *    lineNumber: (?|undefined),
+ *    fileName: (?|undefined),
+ *    stack: (?|undefined)
+ * }} Normalized error object.
+ */
+goog.debug.normalizeErrorObject = function(err) {
+  var href = goog.getObjectByName('window.location.href');
+  if (goog.isString(err)) {
+    return {
+      'message': err,
+      'name': 'Unknown error',
+      'lineNumber': 'Not available',
+      'fileName': href,
+      'stack': 'Not available'
+    };
+  }
+
+  var lineNumber, fileName;
+  var threwError = false;
+
+  try {
+    lineNumber = err.lineNumber || err.line || 'Not available';
+  } catch (e) {
+    // Firefox 2 sometimes throws an error when accessing 'lineNumber':
+    // Message: Permission denied to get property UnnamedClass.lineNumber
+    lineNumber = 'Not available';
+    threwError = true;
+  }
+
+  try {
+    fileName = err.fileName || err.filename || err.sourceURL ||
+        // $googDebugFname may be set before a call to eval to set the filename
+        // that the eval is supposed to present.
+        goog.global['$googDebugFname'] || href;
+  } catch (e) {
+    // Firefox 2 may also throw an error when accessing 'filename'.
+    fileName = 'Not available';
+    threwError = true;
+  }
+
+  // The IE Error object contains only the name and the message.
+  // The Safari Error object uses the line and sourceURL fields.
+  if (threwError || !err.lineNumber || !err.fileName || !err.stack ||
+      !err.message || !err.name) {
+    return {
+      'message': err.message || 'Not available',
+      'name': err.name || 'UnknownError',
+      'lineNumber': lineNumber,
+      'fileName': fileName,
+      'stack': err.stack || 'Not available'
+    };
+  }
+
+  // Standards error object
+  // Typed !Object. Should be a subtype of the return type, but it's not.
+  return /** @type {?} */ (err);
+};
+
+
+/**
+ * Converts an object to an Error using the object's toString if it's not
+ * already an Error, adds a stacktrace if there isn't one, and optionally adds
+ * an extra message.
+ * @param {*} err The original thrown error, object, or string.
+ * @param {string=} opt_message  optional additional message to add to the
+ *     error.
+ * @return {!Error} If err is an Error, it is enhanced and returned. Otherwise,
+ *     it is converted to an Error which is enhanced and returned.
+ */
+goog.debug.enhanceError = function(err, opt_message) {
+  var error;
+  if (!(err instanceof Error)) {
+    error = Error(err);
+    if (Error.captureStackTrace) {
+      // Trim this function off the call stack, if we can.
+      Error.captureStackTrace(error, goog.debug.enhanceError);
+    }
+  } else {
+    error = err;
+  }
+
+  if (!error.stack) {
+    error.stack = goog.debug.getStacktrace(goog.debug.enhanceError);
+  }
+  if (opt_message) {
+    // find the first unoccupied 'messageX' property
+    var x = 0;
+    while (error['message' + x]) {
+      ++x;
+    }
+    error['message' + x] = String(opt_message);
+  }
+  return error;
+};
+
+
+/**
+ * Converts an object to an Error using the object's toString if it's not
+ * already an Error, adds a stacktrace if there isn't one, and optionally adds
+ * context to the Error, which is reported by the closure error reporter.
+ * @param {*} err The original thrown error, object, or string.
+ * @param {!Object<string, string>=} opt_context Key-value context to add to the
+ *     Error.
+ * @return {!Error} If err is an Error, it is enhanced and returned. Otherwise,
+ *     it is converted to an Error which is enhanced and returned.
+ */
+goog.debug.enhanceErrorWithContext = function(err, opt_context) {
+  var error = goog.debug.enhanceError(err);
+  if (opt_context) {
+    for (var key in opt_context) {
+      goog.debug.errorcontext.addErrorContext(error, key, opt_context[key]);
+    }
+  }
+  return error;
+};
+
+
+/**
+ * Gets the current stack trace. Simple and iterative - doesn't worry about
+ * catching circular references or getting the args.
+ * @param {number=} opt_depth Optional maximum depth to trace back to.
+ * @return {string} A string with the function names of all functions in the
+ *     stack, separated by \n.
+ * @suppress {es5Strict}
+ */
+goog.debug.getStacktraceSimple = function(opt_depth) {
+  if (!goog.debug.FORCE_SLOPPY_STACKS) {
+    var stack = goog.debug.getNativeStackTrace_(goog.debug.getStacktraceSimple);
+    if (stack) {
+      return stack;
+    }
+    // NOTE: browsers that have strict mode support also have native "stack"
+    // properties.  Fall-through for legacy browser support.
+  }
+
+  var sb = [];
+  var fn = arguments.callee.caller;
+  var depth = 0;
+
+  while (fn && (!opt_depth || depth < opt_depth)) {
+    sb.push(goog.debug.getFunctionName(fn));
+    sb.push('()\n');
+
+    try {
+      fn = fn.caller;
+    } catch (e) {
+      sb.push('[exception trying to get caller]\n');
+      break;
+    }
+    depth++;
+    if (depth >= goog.debug.MAX_STACK_DEPTH) {
+      sb.push('[...long stack...]');
+      break;
+    }
+  }
+  if (opt_depth && depth >= opt_depth) {
+    sb.push('[...reached max depth limit...]');
+  } else {
+    sb.push('[end]');
+  }
+
+  return sb.join('');
+};
+
+
+/**
+ * Max length of stack to try and output
+ * @type {number}
+ */
+goog.debug.MAX_STACK_DEPTH = 50;
+
+
+/**
+ * @param {Function} fn The function to start getting the trace from.
+ * @return {?string}
+ * @private
+ */
+goog.debug.getNativeStackTrace_ = function(fn) {
+  var tempErr = new Error();
+  if (Error.captureStackTrace) {
+    Error.captureStackTrace(tempErr, fn);
+    return String(tempErr.stack);
+  } else {
+    // IE10, only adds stack traces when an exception is thrown.
+    try {
+      throw tempErr;
+    } catch (e) {
+      tempErr = e;
+    }
+    var stack = tempErr.stack;
+    if (stack) {
+      return String(stack);
+    }
+  }
+  return null;
+};
+
+
+/**
+ * Gets the current stack trace, either starting from the caller or starting
+ * from a specified function that's currently on the call stack.
+ * @param {?Function=} fn If provided, when collecting the stack trace all
+ *     frames above the topmost call to this function, including that call,
+ *     will be left out of the stack trace.
+ * @return {string} Stack trace.
+ * @suppress {es5Strict}
+ */
+goog.debug.getStacktrace = function(fn) {
+  var stack;
+  if (!goog.debug.FORCE_SLOPPY_STACKS) {
+    // Try to get the stack trace from the environment if it is available.
+    var contextFn = fn || goog.debug.getStacktrace;
+    stack = goog.debug.getNativeStackTrace_(contextFn);
+  }
+  if (!stack) {
+    // NOTE: browsers that have strict mode support also have native "stack"
+    // properties. This function will throw in strict mode.
+    stack = goog.debug.getStacktraceHelper_(fn || arguments.callee.caller, []);
+  }
+  return stack;
+};
+
+
+/**
+ * Private helper for getStacktrace().
+ * @param {?Function} fn If provided, when collecting the stack trace all
+ *     frames above the topmost call to this function, including that call,
+ *     will be left out of the stack trace.
+ * @param {Array<!Function>} visited List of functions visited so far.
+ * @return {string} Stack trace starting from function fn.
+ * @suppress {es5Strict}
+ * @private
+ */
+goog.debug.getStacktraceHelper_ = function(fn, visited) {
+  var sb = [];
+
+  // Circular reference, certain functions like bind seem to cause a recursive
+  // loop so we need to catch circular references
+  if (goog.array.contains(visited, fn)) {
+    sb.push('[...circular reference...]');
+
+    // Traverse the call stack until function not found or max depth is reached
+  } else if (fn && visited.length < goog.debug.MAX_STACK_DEPTH) {
+    sb.push(goog.debug.getFunctionName(fn) + '(');
+    var args = fn.arguments;
+    // Args may be null for some special functions such as host objects or eval.
+    for (var i = 0; args && i < args.length; i++) {
+      if (i > 0) {
+        sb.push(', ');
+      }
+      var argDesc;
+      var arg = args[i];
+      switch (typeof arg) {
+        case 'object':
+          argDesc = arg ? 'object' : 'null';
+          break;
+
+        case 'string':
+          argDesc = arg;
+          break;
+
+        case 'number':
+          argDesc = String(arg);
+          break;
+
+        case 'boolean':
+          argDesc = arg ? 'true' : 'false';
+          break;
+
+        case 'function':
+          argDesc = goog.debug.getFunctionName(arg);
+          argDesc = argDesc ? argDesc : '[fn]';
+          break;
+
+        case 'undefined':
+        default:
+          argDesc = typeof arg;
+          break;
+      }
+
+      if (argDesc.length > 40) {
+        argDesc = argDesc.substr(0, 40) + '...';
+      }
+      sb.push(argDesc);
+    }
+    visited.push(fn);
+    sb.push(')\n');
+
+    try {
+      sb.push(goog.debug.getStacktraceHelper_(fn.caller, visited));
+    } catch (e) {
+      sb.push('[exception trying to get caller]\n');
+    }
+
+  } else if (fn) {
+    sb.push('[...long stack...]');
+  } else {
+    sb.push('[end]');
+  }
+  return sb.join('');
+};
+
+
+/**
+ * Set a custom function name resolver.
+ * @param {function(Function): string} resolver Resolves functions to their
+ *     names.
+ */
+goog.debug.setFunctionResolver = function(resolver) {
+  goog.debug.fnNameResolver_ = resolver;
+};
+
+
+/**
+ * Gets a function name
+ * @param {Function} fn Function to get name of.
+ * @return {string} Function's name.
+ */
+goog.debug.getFunctionName = function(fn) {
+  if (goog.debug.fnNameCache_[fn]) {
+    return goog.debug.fnNameCache_[fn];
+  }
+  if (goog.debug.fnNameResolver_) {
+    var name = goog.debug.fnNameResolver_(fn);
+    if (name) {
+      goog.debug.fnNameCache_[fn] = name;
+      return name;
+    }
+  }
+
+  // Heuristically determine function name based on code.
+  var functionSource = String(fn);
+  if (!goog.debug.fnNameCache_[functionSource]) {
+    var matches = /function ([^\(]+)/.exec(functionSource);
+    if (matches) {
+      var method = matches[1];
+      goog.debug.fnNameCache_[functionSource] = method;
+    } else {
+      goog.debug.fnNameCache_[functionSource] = '[Anonymous]';
+    }
+  }
+
+  return goog.debug.fnNameCache_[functionSource];
+};
+
+
+/**
+ * Makes whitespace visible by replacing it with printable characters.
+ * This is useful in finding diffrences between the expected and the actual
+ * output strings of a testcase.
+ * @param {string} string whose whitespace needs to be made visible.
+ * @return {string} string whose whitespace is made visible.
+ */
+goog.debug.makeWhitespaceVisible = function(string) {
+  return string.replace(/ /g, '[_]')
+      .replace(/\f/g, '[f]')
+      .replace(/\n/g, '[n]\n')
+      .replace(/\r/g, '[r]')
+      .replace(/\t/g, '[t]');
+};
+
+
+/**
+ * Returns the type of a value. If a constructor is passed, and a suitable
+ * string cannot be found, 'unknown type name' will be returned.
+ *
+ * <p>Forked rather than moved from {@link goog.asserts.getType_}
+ * to avoid adding a dependency to goog.asserts.
+ * @param {*} value A constructor, object, or primitive.
+ * @return {string} The best display name for the value, or 'unknown type name'.
+ */
+goog.debug.runtimeType = function(value) {
+  if (value instanceof Function) {
+    return value.displayName || value.name || 'unknown type name';
+  } else if (value instanceof Object) {
+    return value.constructor.displayName || value.constructor.name ||
+        Object.prototype.toString.call(value);
+  } else {
+    return value === null ? 'null' : typeof value;
+  }
+};
+
+
+/**
+ * Hash map for storing function names that have already been looked up.
+ * @type {Object}
+ * @private
+ */
+goog.debug.fnNameCache_ = {};
+
+
+/**
+ * Resolves functions to their names.  Resolved function names will be cached.
+ * @type {function(Function):string}
+ * @private
+ */
+goog.debug.fnNameResolver_;
+
+
+/**
+ * Private internal function to support goog.debug.freeze.
+ * @param {T} arg
+ * @return {T}
+ * @template T
+ * @private
+ */
+goog.debug.freezeInternal_ = goog.DEBUG && Object.freeze || function(arg) {
+  return arg;
+};
+
+
+/**
+ * Freezes the given object, but only in debug mode (and in browsers that
+ * support it).  Note that this is a shallow freeze, so for deeply nested
+ * objects it must be called at every level to ensure deep immutability.
+ * @param {T} arg
+ * @return {T}
+ * @template T
+ */
+goog.debug.freeze = function(arg) {
+  // NOTE: this compiles to nothing, but hides the possible side effect of
+  // freezeInternal_ from the compiler so that the entire call can be
+  // removed if the result is not used.
+  return {
+    valueOf: function() {
+      return goog.debug.freezeInternal_(arg);
+    }
+  }.valueOf();
+};
diff --git a/third_party/ink/closure/debug/entrypointregistry.js b/third_party/ink/closure/debug/entrypointregistry.js
new file mode 100644
index 0000000..336e1468
--- /dev/null
+++ b/third_party/ink/closure/debug/entrypointregistry.js
@@ -0,0 +1,159 @@
+// Copyright 2010 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 A global registry for entry points into a program,
+ * so that they can be instrumented. Each module should register their
+ * entry points with this registry. Designed to be compiled out
+ * if no instrumentation is requested.
+ *
+ * Entry points may be registered before or after a call to
+ * goog.debug.entryPointRegistry.monitorAll. If an entry point is registered
+ * later, the existing monitor will instrument the new entry point.
+ *
+ * @author nicksantos@google.com (Nick Santos)
+ */
+
+goog.provide('goog.debug.EntryPointMonitor');
+goog.provide('goog.debug.entryPointRegistry');
+
+goog.require('goog.asserts');
+
+
+
+/**
+ * @interface
+ */
+goog.debug.EntryPointMonitor = function() {};
+
+
+/**
+ * Instruments a function.
+ *
+ * @param {!Function} fn A function to instrument.
+ * @return {!Function} The instrumented function.
+ */
+goog.debug.EntryPointMonitor.prototype.wrap;
+
+
+/**
+ * Try to remove an instrumentation wrapper created by this monitor.
+ * If the function passed to unwrap is not a wrapper created by this
+ * monitor, then we will do nothing.
+ *
+ * Notice that some wrappers may not be unwrappable. For example, if other
+ * monitors have applied their own wrappers, then it will be impossible to
+ * unwrap them because their wrappers will have captured our wrapper.
+ *
+ * So it is important that entry points are unwrapped in the reverse
+ * order that they were wrapped.
+ *
+ * @param {!Function} fn A function to unwrap.
+ * @return {!Function} The unwrapped function, or {@code fn} if it was not
+ *     a wrapped function created by this monitor.
+ */
+goog.debug.EntryPointMonitor.prototype.unwrap;
+
+
+/**
+ * An array of entry point callbacks.
+ * @type {!Array<function(!Function)>}
+ * @private
+ */
+goog.debug.entryPointRegistry.refList_ = [];
+
+
+/**
+ * Monitors that should wrap all the entry points.
+ * @type {!Array<!goog.debug.EntryPointMonitor>}
+ * @private
+ */
+goog.debug.entryPointRegistry.monitors_ = [];
+
+
+/**
+ * Whether goog.debug.entryPointRegistry.monitorAll has ever been called.
+ * Checking this allows the compiler to optimize out the registrations.
+ * @type {boolean}
+ * @private
+ */
+goog.debug.entryPointRegistry.monitorsMayExist_ = false;
+
+
+/**
+ * Register an entry point with this module.
+ *
+ * The entry point will be instrumented when a monitor is passed to
+ * goog.debug.entryPointRegistry.monitorAll. If this has already occurred, the
+ * entry point is instrumented immediately.
+ *
+ * @param {function(!Function)} callback A callback function which is called
+ *     with a transforming function to instrument the entry point. The callback
+ *     is responsible for wrapping the relevant entry point with the
+ *     transforming function.
+ */
+goog.debug.entryPointRegistry.register = function(callback) {
+  // Don't use push(), so that this can be compiled out.
+  goog.debug.entryPointRegistry
+      .refList_[goog.debug.entryPointRegistry.refList_.length] = callback;
+  // If no one calls monitorAll, this can be compiled out.
+  if (goog.debug.entryPointRegistry.monitorsMayExist_) {
+    var monitors = goog.debug.entryPointRegistry.monitors_;
+    for (var i = 0; i < monitors.length; i++) {
+      callback(goog.bind(monitors[i].wrap, monitors[i]));
+    }
+  }
+};
+
+
+/**
+ * Configures a monitor to wrap all entry points.
+ *
+ * Entry points that have already been registered are immediately wrapped by
+ * the monitor. When an entry point is registered in the future, it will also
+ * be wrapped by the monitor when it is registered.
+ *
+ * @param {!goog.debug.EntryPointMonitor} monitor An entry point monitor.
+ */
+goog.debug.entryPointRegistry.monitorAll = function(monitor) {
+  goog.debug.entryPointRegistry.monitorsMayExist_ = true;
+  var transformer = goog.bind(monitor.wrap, monitor);
+  for (var i = 0; i < goog.debug.entryPointRegistry.refList_.length; i++) {
+    goog.debug.entryPointRegistry.refList_[i](transformer);
+  }
+  goog.debug.entryPointRegistry.monitors_.push(monitor);
+};
+
+
+/**
+ * Try to unmonitor all the entry points that have already been registered. If
+ * an entry point is registered in the future, it will not be wrapped by the
+ * monitor when it is registered. Note that this may fail if the entry points
+ * have additional wrapping.
+ *
+ * @param {!goog.debug.EntryPointMonitor} monitor The last monitor to wrap
+ *     the entry points.
+ * @throws {Error} If the monitor is not the most recently configured monitor.
+ */
+goog.debug.entryPointRegistry.unmonitorAllIfPossible = function(monitor) {
+  var monitors = goog.debug.entryPointRegistry.monitors_;
+  goog.asserts.assert(
+      monitor == monitors[monitors.length - 1],
+      'Only the most recent monitor can be unwrapped.');
+  var transformer = goog.bind(monitor.unwrap, monitor);
+  for (var i = 0; i < goog.debug.entryPointRegistry.refList_.length; i++) {
+    goog.debug.entryPointRegistry.refList_[i](transformer);
+  }
+  monitors.length--;
+};
diff --git a/third_party/ink/closure/debug/error.js b/third_party/ink/closure/debug/error.js
new file mode 100644
index 0000000..2099b56
--- /dev/null
+++ b/third_party/ink/closure/debug/error.js
@@ -0,0 +1,66 @@
+// Copyright 2009 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 Provides a base class for custom Error objects such that the
+ * stack is correctly maintained.
+ *
+ * You should never need to throw goog.debug.Error(msg) directly, Error(msg) is
+ * sufficient.
+ *
+ * @author pupius@google.com (Daniel Pupius)
+ */
+
+goog.provide('goog.debug.Error');
+
+
+
+/**
+ * Base class for custom error objects.
+ * @param {*=} opt_msg The message associated with the error.
+ * @constructor
+ * @extends {Error}
+ */
+goog.debug.Error = function(opt_msg) {
+
+  // Attempt to ensure there is a stack trace.
+  if (Error.captureStackTrace) {
+    Error.captureStackTrace(this, goog.debug.Error);
+  } else {
+    var stack = new Error().stack;
+    if (stack) {
+      /** @override */
+      this.stack = stack;
+    }
+  }
+
+  if (opt_msg) {
+    /** @override */
+    this.message = String(opt_msg);
+  }
+
+  /**
+   * Whether to report this error to the server. Setting this to false will
+   * cause the error reporter to not report the error back to the server,
+   * which can be useful if the client knows that the error has already been
+   * logged on the server.
+   * @type {boolean}
+   */
+  this.reportErrorToServer = true;
+};
+goog.inherits(goog.debug.Error, Error);
+
+
+/** @override */
+goog.debug.Error.prototype.name = 'CustomError';
diff --git a/third_party/ink/closure/debug/errorcontext.js b/third_party/ink/closure/debug/errorcontext.js
new file mode 100644
index 0000000..683454a3
--- /dev/null
+++ b/third_party/ink/closure/debug/errorcontext.js
@@ -0,0 +1,49 @@
+// Copyright 2017 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 Provides methods dealing with context on error objects.
+ */
+
+goog.provide('goog.debug.errorcontext');
+
+
+/**
+ * Adds key-value context to the error.
+ * @param {!Error} err The error to add context to.
+ * @param {string} contextKey Key for the context to be added.
+ * @param {string} contextValue Value for the context to be added.
+ */
+goog.debug.errorcontext.addErrorContext = function(
+    err, contextKey, contextValue) {
+  if (!err[goog.debug.errorcontext.CONTEXT_KEY_]) {
+    err[goog.debug.errorcontext.CONTEXT_KEY_] = {};
+  }
+  err[goog.debug.errorcontext.CONTEXT_KEY_][contextKey] = contextValue;
+};
+
+
+/**
+ * @param {!Error} err The error to get context from.
+ * @return {!Object<string, string>} The context of the provided error.
+ */
+goog.debug.errorcontext.getErrorContext = function(err) {
+  return err[goog.debug.errorcontext.CONTEXT_KEY_] || {};
+};
+
+
+// TODO(aaronsn): convert this to a Symbol once goog.debug.ErrorReporter is
+// able to use ES6.
+/** @private @const {string} */
+goog.debug.errorcontext.CONTEXT_KEY_ = '__closure__error__context__984382';
diff --git a/third_party/ink/closure/disposable/disposable.js b/third_party/ink/closure/disposable/disposable.js
new file mode 100644
index 0000000..b3e8138
--- /dev/null
+++ b/third_party/ink/closure/disposable/disposable.js
@@ -0,0 +1,305 @@
+// Copyright 2005 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 Implements the disposable interface. The dispose method is used
+ * to clean up references and resources.
+ * @author arv@google.com (Erik Arvidsson)
+ */
+
+
+goog.provide('goog.Disposable');
+goog.provide('goog.dispose');
+goog.provide('goog.disposeAll');
+
+goog.require('goog.disposable.IDisposable');
+
+
+
+/**
+ * Class that provides the basic implementation for disposable objects. If your
+ * class holds one or more references to COM objects, DOM nodes, or other
+ * disposable objects, it should extend this class or implement the disposable
+ * interface (defined in goog.disposable.IDisposable).
+ * @constructor
+ * @implements {goog.disposable.IDisposable}
+ */
+goog.Disposable = function() {
+  /**
+   * If monitoring the goog.Disposable instances is enabled, stores the creation
+   * stack trace of the Disposable instance.
+   * @type {string|undefined}
+   */
+  this.creationStack;
+
+  if (goog.Disposable.MONITORING_MODE != goog.Disposable.MonitoringMode.OFF) {
+    if (goog.Disposable.INCLUDE_STACK_ON_CREATION) {
+      this.creationStack = new Error().stack;
+    }
+    goog.Disposable.instances_[goog.getUid(this)] = this;
+  }
+  // Support sealing
+  this.disposed_ = this.disposed_;
+  this.onDisposeCallbacks_ = this.onDisposeCallbacks_;
+};
+
+
+/**
+ * @enum {number} Different monitoring modes for Disposable.
+ */
+goog.Disposable.MonitoringMode = {
+  /**
+   * No monitoring.
+   */
+  OFF: 0,
+  /**
+   * Creating and disposing the goog.Disposable instances is monitored. All
+   * disposable objects need to call the {@code goog.Disposable} base
+   * constructor. The PERMANENT mode must be switched on before creating any
+   * goog.Disposable instances.
+   */
+  PERMANENT: 1,
+  /**
+   * INTERACTIVE mode can be switched on and off on the fly without producing
+   * errors. It also doesn't warn if the disposable objects don't call the
+   * {@code goog.Disposable} base constructor.
+   */
+  INTERACTIVE: 2
+};
+
+
+/**
+ * @define {number} The monitoring mode of the goog.Disposable
+ *     instances. Default is OFF. Switching on the monitoring is only
+ *     recommended for debugging because it has a significant impact on
+ *     performance and memory usage. If switched off, the monitoring code
+ *     compiles down to 0 bytes.
+ */
+goog.define('goog.Disposable.MONITORING_MODE', 0);
+
+
+/**
+ * @define {boolean} Whether to attach creation stack to each created disposable
+ *     instance; This is only relevant for when MonitoringMode != OFF.
+ */
+goog.define('goog.Disposable.INCLUDE_STACK_ON_CREATION', true);
+
+
+/**
+ * Maps the unique ID of every undisposed {@code goog.Disposable} object to
+ * the object itself.
+ * @type {!Object<number, !goog.Disposable>}
+ * @private
+ */
+goog.Disposable.instances_ = {};
+
+
+/**
+ * @return {!Array<!goog.Disposable>} All {@code goog.Disposable} objects that
+ *     haven't been disposed of.
+ */
+goog.Disposable.getUndisposedObjects = function() {
+  var ret = [];
+  for (var id in goog.Disposable.instances_) {
+    if (goog.Disposable.instances_.hasOwnProperty(id)) {
+      ret.push(goog.Disposable.instances_[Number(id)]);
+    }
+  }
+  return ret;
+};
+
+
+/**
+ * Clears the registry of undisposed objects but doesn't dispose of them.
+ */
+goog.Disposable.clearUndisposedObjects = function() {
+  goog.Disposable.instances_ = {};
+};
+
+
+/**
+ * Whether the object has been disposed of.
+ * @type {boolean}
+ * @private
+ */
+goog.Disposable.prototype.disposed_ = false;
+
+
+/**
+ * Callbacks to invoke when this object is disposed.
+ * @type {Array<!Function>}
+ * @private
+ */
+goog.Disposable.prototype.onDisposeCallbacks_;
+
+
+/**
+ * @return {boolean} Whether the object has been disposed of.
+ * @override
+ */
+goog.Disposable.prototype.isDisposed = function() {
+  return this.disposed_;
+};
+
+
+/**
+ * @return {boolean} Whether the object has been disposed of.
+ * @deprecated Use {@link #isDisposed} instead.
+ */
+goog.Disposable.prototype.getDisposed = goog.Disposable.prototype.isDisposed;
+
+
+/**
+ * Disposes of the object. If the object hasn't already been disposed of, calls
+ * {@link #disposeInternal}. Classes that extend {@code goog.Disposable} should
+ * override {@link #disposeInternal} in order to delete references to COM
+ * objects, DOM nodes, and other disposable objects. Reentrant.
+ *
+ * @return {void} Nothing.
+ * @override
+ */
+goog.Disposable.prototype.dispose = function() {
+  if (!this.disposed_) {
+    // Set disposed_ to true first, in case during the chain of disposal this
+    // gets disposed recursively.
+    this.disposed_ = true;
+    this.disposeInternal();
+    if (goog.Disposable.MONITORING_MODE != goog.Disposable.MonitoringMode.OFF) {
+      var uid = goog.getUid(this);
+      if (goog.Disposable.MONITORING_MODE ==
+              goog.Disposable.MonitoringMode.PERMANENT &&
+          !goog.Disposable.instances_.hasOwnProperty(uid)) {
+        throw new Error(
+            this + ' did not call the goog.Disposable base ' +
+            'constructor or was disposed of after a clearUndisposedObjects ' +
+            'call');
+      }
+      delete goog.Disposable.instances_[uid];
+    }
+  }
+};
+
+
+/**
+ * Associates a disposable object with this object so that they will be disposed
+ * together.
+ * @param {goog.disposable.IDisposable} disposable that will be disposed when
+ *     this object is disposed.
+ */
+goog.Disposable.prototype.registerDisposable = function(disposable) {
+  this.addOnDisposeCallback(goog.partial(goog.dispose, disposable));
+};
+
+
+/**
+ * Invokes a callback function when this object is disposed. Callbacks are
+ * invoked in the order in which they were added. If a callback is added to
+ * an already disposed Disposable, it will be called immediately.
+ * @param {function(this:T):?} callback The callback function.
+ * @param {T=} opt_scope An optional scope to call the callback in.
+ * @template T
+ */
+goog.Disposable.prototype.addOnDisposeCallback = function(callback, opt_scope) {
+  if (this.disposed_) {
+    goog.isDef(opt_scope) ? callback.call(opt_scope) : callback();
+    return;
+  }
+  if (!this.onDisposeCallbacks_) {
+    this.onDisposeCallbacks_ = [];
+  }
+
+  this.onDisposeCallbacks_.push(
+      goog.isDef(opt_scope) ? goog.bind(callback, opt_scope) : callback);
+};
+
+
+/**
+ * Deletes or nulls out any references to COM objects, DOM nodes, or other
+ * disposable objects. Classes that extend {@code goog.Disposable} should
+ * override this method.
+ * Not reentrant. To avoid calling it twice, it must only be called from the
+ * subclass' {@code disposeInternal} method. Everywhere else the public
+ * {@code dispose} method must be used.
+ * For example:
+ * <pre>
+ *   mypackage.MyClass = function() {
+ *     mypackage.MyClass.base(this, 'constructor');
+ *     // Constructor logic specific to MyClass.
+ *     ...
+ *   };
+ *   goog.inherits(mypackage.MyClass, goog.Disposable);
+ *
+ *   mypackage.MyClass.prototype.disposeInternal = function() {
+ *     // Dispose logic specific to MyClass.
+ *     ...
+ *     // Call superclass's disposeInternal at the end of the subclass's, like
+ *     // in C++, to avoid hard-to-catch issues.
+ *     mypackage.MyClass.base(this, 'disposeInternal');
+ *   };
+ * </pre>
+ * @protected
+ */
+goog.Disposable.prototype.disposeInternal = function() {
+  if (this.onDisposeCallbacks_) {
+    while (this.onDisposeCallbacks_.length) {
+      this.onDisposeCallbacks_.shift()();
+    }
+  }
+};
+
+
+/**
+ * Returns True if we can verify the object is disposed.
+ * Calls {@code isDisposed} on the argument if it supports it.  If obj
+ * is not an object with an isDisposed() method, return false.
+ * @param {*} obj The object to investigate.
+ * @return {boolean} True if we can verify the object is disposed.
+ */
+goog.Disposable.isDisposed = function(obj) {
+  if (obj && typeof obj.isDisposed == 'function') {
+    return obj.isDisposed();
+  }
+  return false;
+};
+
+
+/**
+ * Calls {@code dispose} on the argument if it supports it. If obj is not an
+ *     object with a dispose() method, this is a no-op.
+ * @param {*} obj The object to dispose of.
+ */
+goog.dispose = function(obj) {
+  if (obj && typeof obj.dispose == 'function') {
+    obj.dispose();
+  }
+};
+
+
+/**
+ * Calls {@code dispose} on each member of the list that supports it. (If the
+ * member is an ArrayLike, then {@code goog.disposeAll()} will be called
+ * recursively on each of its members.) If the member is not an object with a
+ * {@code dispose()} method, then it is ignored.
+ * @param {...*} var_args The list.
+ */
+goog.disposeAll = function(var_args) {
+  for (var i = 0, len = arguments.length; i < len; ++i) {
+    var disposable = arguments[i];
+    if (goog.isArrayLike(disposable)) {
+      goog.disposeAll.apply(null, disposable);
+    } else {
+      goog.dispose(disposable);
+    }
+  }
+};
diff --git a/third_party/ink/closure/disposable/idisposable.js b/third_party/ink/closure/disposable/idisposable.js
new file mode 100644
index 0000000..b539eb6
--- /dev/null
+++ b/third_party/ink/closure/disposable/idisposable.js
@@ -0,0 +1,45 @@
+// 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 Definition of the disposable interface.  A disposable object
+ * has a dispose method to to clean up references and resources.
+ * @author nnaze@google.com (Nathan Naze)
+ */
+
+
+goog.provide('goog.disposable.IDisposable');
+
+
+
+/**
+ * Interface for a disposable object.  If a instance requires cleanup
+ * (references COM objects, DOM nodes, or other disposable objects), it should
+ * implement this interface (it may subclass goog.Disposable).
+ * @record
+ */
+goog.disposable.IDisposable = function() {};
+
+
+/**
+ * Disposes of the object and its resources.
+ * @return {void} Nothing.
+ */
+goog.disposable.IDisposable.prototype.dispose = goog.abstractMethod;
+
+
+/**
+ * @return {boolean} Whether the object has been disposed of.
+ */
+goog.disposable.IDisposable.prototype.isDisposed = goog.abstractMethod;
diff --git a/third_party/ink/closure/dom/asserts.js b/third_party/ink/closure/dom/asserts.js
new file mode 100644
index 0000000..e891440
--- /dev/null
+++ b/third_party/ink/closure/dom/asserts.js
@@ -0,0 +1,299 @@
+// Copyright 2017 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.
+
+goog.provide('goog.dom.asserts');
+
+goog.require('goog.asserts');
+
+/**
+ * @fileoverview Custom assertions to ensure that an element has the appropriate
+ * type.
+ *
+ * Using a goog.dom.safe wrapper on an object on the incorrect type (via an
+ * incorrect static type cast) can result in security bugs: For instance,
+ * g.d.s.setAnchorHref ensures that the URL assigned to the .href attribute
+ * satisfies the SafeUrl contract, i.e., is safe to dereference as a hyperlink.
+ * However, the value assigned to a HTMLLinkElement's .href property requires
+ * the stronger TrustedResourceUrl contract, since it can refer to a stylesheet.
+ * Thus, using g.d.s.setAnchorHref on an (incorrectly statically typed) object
+ * of type HTMLLinkElement can result in a security vulnerability.
+ * Assertions of the correct run-time type help prevent such incorrect use.
+ *
+ * In some cases, code using the DOM API is tested using mock objects (e.g., a
+ * plain object such as {'href': url} instead of an actual Location object).
+ * To allow such mocking, the assertions permit objects of types that are not
+ * relevant DOM API objects at all (for instance, not Element or Location).
+ *
+ * Note that instanceof checks don't work straightforwardly in older versions of
+ * IE, or across frames (see,
+ * http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object,
+ * http://stackoverflow.com/questions/26248599/instanceof-htmlelement-in-iframe-is-not-element-or-object).
+ *
+ * Hence, these assertions may pass vacuously in such scenarios. The resulting
+ * risk of security bugs is limited by the following factors:
+ *  - A bug can only arise in scenarios involving incorrect static typing (the
+ *    wrapper methods are statically typed to demand objects of the appropriate,
+ *    precise type).
+ *  - Typically, code is tested and exercised in multiple browsers.
+ */
+
+/**
+ * Asserts that a given object is a Location.
+ *
+ * To permit this assertion to pass in the context of tests where DOM APIs might
+ * be mocked, also accepts any other type except for subtypes of {!Element}.
+ * This is to ensure that, for instance, HTMLLinkElement is not being used in
+ * place of a Location, since this could result in security bugs due to stronger
+ * contracts required for assignments to the href property of the latter.
+ *
+ * @param {?Object} o The object whose type to assert.
+ * @return {!Location}
+ */
+goog.dom.asserts.assertIsLocation = function(o) {
+  if (goog.asserts.ENABLE_ASSERTS) {
+    var win = goog.dom.asserts.getWindow_(o);
+    if (typeof win.Location != 'undefined' &&
+        typeof win.Element != 'undefined') {
+      goog.asserts.assert(
+          o && (o instanceof win.Location || !(o instanceof win.Element)),
+          'Argument is not a Location (or a non-Element mock); got: %s',
+          goog.dom.asserts.debugStringForType_(o));
+    }
+  }
+  return /** @type {!Location} */ (o);
+};
+
+
+/**
+ * Asserts that a given object is either the given subtype of Element
+ * or a non-Element, non-Location Mock.
+ *
+ * To permit this assertion to pass in the context of tests where DOM
+ * APIs might be mocked, also accepts any other type except for
+ * subtypes of {!Element}.  This is to ensure that, for instance,
+ * HTMLScriptElement is not being used in place of a HTMLImageElement,
+ * since this could result in security bugs due to stronger contracts
+ * required for assignments to the src property of the latter.
+ *
+ * The DOM type is looked up in the window the object belongs to.  In
+ * some contexts, this might not be possible (e.g. when running tests
+ * outside a browser, cross-domain lookup). In this case, the
+ * assertions are skipped.
+ *
+ * @param {?Object} o The object whose type to assert.
+ * @param {string} typename The name of the DOM type.
+ * @return {!Element} The object.
+ * @private
+ */
+// TODO(bangert): Make an analog of goog.dom.TagName to correctly handle casts?
+goog.dom.asserts.assertIsElementType_ = function(o, typename) {
+  if (goog.asserts.ENABLE_ASSERTS) {
+    var win = goog.dom.asserts.getWindow_(o);
+    if (typeof win[typename] != 'undefined' &&
+        typeof win.Location != 'undefined' &&
+        typeof win.Element != 'undefined') {
+      goog.asserts.assert(
+          o &&
+              (o instanceof win[typename] ||
+               !((o instanceof win.Location) || (o instanceof win.Element))),
+          'Argument is not a %s (or a non-Element, non-Location mock); got: %s',
+          typename, goog.dom.asserts.debugStringForType_(o));
+    }
+  }
+  return /** @type {!Element} */ (o);
+};
+
+/**
+ * Asserts that a given object is a HTMLAnchorElement.
+ *
+ * To permit this assertion to pass in the context of tests where elements might
+ * be mocked, also accepts objects that are not of type Location nor a subtype
+ * of Element.
+ *
+ * @param {?Object} o The object whose type to assert.
+ * @return {!HTMLAnchorElement}
+ */
+goog.dom.asserts.assertIsHTMLAnchorElement = function(o) {
+  return /** @type {!HTMLAnchorElement} */ (
+      goog.dom.asserts.assertIsElementType_(o, 'HTMLAnchorElement'));
+};
+
+/**
+ * Asserts that a given object is a HTMLButtonElement.
+ *
+ * To permit this assertion to pass in the context of tests where elements might
+ * be mocked, also accepts objects that are not a subtype of Element.
+ *
+ * @param {?Object} o The object whose type to assert.
+ * @return {!HTMLButtonElement}
+ */
+goog.dom.asserts.assertIsHTMLButtonElement = function(o) {
+  return /** @type {!HTMLButtonElement} */ (
+      goog.dom.asserts.assertIsElementType_(o, 'HTMLButtonElement'));
+};
+
+/**
+ * Asserts that a given object is a HTMLLinkElement.
+ *
+ * To permit this assertion to pass in the context of tests where elements might
+ * be mocked, also accepts objects that are not a subtype of Element.
+ *
+ * @param {?Object} o The object whose type to assert.
+ * @return {!HTMLLinkElement}
+ */
+goog.dom.asserts.assertIsHTMLLinkElement = function(o) {
+  return /** @type {!HTMLLinkElement} */ (
+      goog.dom.asserts.assertIsElementType_(o, 'HTMLLinkElement'));
+};
+
+/**
+ * Asserts that a given object is a HTMLImageElement.
+ *
+ * To permit this assertion to pass in the context of tests where elements might
+ * be mocked, also accepts objects that are not a subtype of Element.
+ *
+ * @param {?Object} o The object whose type to assert.
+ * @return {!HTMLImageElement}
+ */
+goog.dom.asserts.assertIsHTMLImageElement = function(o) {
+  return /** @type {!HTMLImageElement} */ (
+      goog.dom.asserts.assertIsElementType_(o, 'HTMLImageElement'));
+};
+
+/**
+ * Asserts that a given object is a HTMLInputElement.
+ *
+ * To permit this assertion to pass in the context of tests where elements might
+ * be mocked, also accepts objects that are not a subtype of Element.
+ *
+ * @param {?Object} o The object whose type to assert.
+ * @return {!HTMLInputElement}
+ */
+goog.dom.asserts.assertIsHTMLInputElement = function(o) {
+  return /** @type {!HTMLInputElement} */ (
+      goog.dom.asserts.assertIsElementType_(o, 'HTMLInputElement'));
+};
+
+/**
+ * Asserts that a given object is a HTMLEmbedElement.
+ *
+ * To permit this assertion to pass in the context of tests where elements might
+ * be mocked, also accepts objects that are not a subtype of Element.
+ *
+ * @param {?Object} o The object whose type to assert.
+ * @return {!HTMLEmbedElement}
+ */
+goog.dom.asserts.assertIsHTMLEmbedElement = function(o) {
+  return /** @type {!HTMLEmbedElement} */ (
+      goog.dom.asserts.assertIsElementType_(o, 'HTMLEmbedElement'));
+};
+
+/**
+ * Asserts that a given object is a HTMLFormElement.
+ *
+ * To permit this assertion to pass in the context of tests where elements might
+ * be mocked, also accepts objects that are not a subtype of Element.
+ *
+ * @param {?Object} o The object whose type to assert.
+ * @return {!HTMLFormElement}
+ */
+goog.dom.asserts.assertIsHTMLFormElement = function(o) {
+  return /** @type {!HTMLFormElement} */ (
+      goog.dom.asserts.assertIsElementType_(o, 'HTMLFormElement'));
+};
+
+/**
+ * Asserts that a given object is a HTMLFrameElement.
+ *
+ * To permit this assertion to pass in the context of tests where elements might
+ * be mocked, also accepts objects that are not a subtype of Element.
+ *
+ * @param {?Object} o The object whose type to assert.
+ * @return {!HTMLFrameElement}
+ */
+goog.dom.asserts.assertIsHTMLFrameElement = function(o) {
+  return /** @type {!HTMLFrameElement} */ (
+      goog.dom.asserts.assertIsElementType_(o, 'HTMLFrameElement'));
+};
+
+/**
+ * Asserts that a given object is a HTMLIFrameElement.
+ *
+ * To permit this assertion to pass in the context of tests where elements might
+ * be mocked, also accepts objects that are not a subtype of Element.
+ *
+ * @param {?Object} o The object whose type to assert.
+ * @return {!HTMLIFrameElement}
+ */
+goog.dom.asserts.assertIsHTMLIFrameElement = function(o) {
+  return /** @type {!HTMLIFrameElement} */ (
+      goog.dom.asserts.assertIsElementType_(o, 'HTMLIFrameElement'));
+};
+
+/**
+ * Asserts that a given object is a HTMLObjectElement.
+ *
+ * To permit this assertion to pass in the context of tests where elements might
+ * be mocked, also accepts objects that are not a subtype of Element.
+ *
+ * @param {?Object} o The object whose type to assert.
+ * @return {!HTMLObjectElement}
+ */
+goog.dom.asserts.assertIsHTMLObjectElement = function(o) {
+  return /** @type {!HTMLObjectElement} */ (
+      goog.dom.asserts.assertIsElementType_(o, 'HTMLObjectElement'));
+};
+
+/**
+ * Asserts that a given object is a HTMLScriptElement.
+ *
+ * To permit this assertion to pass in the context of tests where elements might
+ * be mocked, also accepts objects that are not a subtype of Element.
+ *
+ * @param {?Object} o The object whose type to assert.
+ * @return {!HTMLScriptElement}
+ */
+goog.dom.asserts.assertIsHTMLScriptElement = function(o) {
+  return /** @type {!HTMLScriptElement} */ (
+      goog.dom.asserts.assertIsElementType_(o, 'HTMLScriptElement'));
+};
+
+/**
+ * Returns a string representation of a value's type.
+ *
+ * @param {*} value An object, or primitive.
+ * @return {string} The best display name for the value.
+ * @private
+ */
+goog.dom.asserts.debugStringForType_ = function(value) {
+  if (goog.isObject(value)) {
+    return value.constructor.displayName || value.constructor.name ||
+        Object.prototype.toString.call(value);
+  } else {
+    return value === undefined ? 'undefined' :
+                                 value === null ? 'null' : typeof value;
+  }
+};
+
+/**
+ * Gets window of element.
+ * @param {?Object} o
+ * @return {!Window}
+ * @private
+ */
+goog.dom.asserts.getWindow_ = function(o) {
+  var doc = o && o.ownerDocument;
+  var win = doc && /** @type {?Window} */ (doc.defaultView || doc.parentWindow);
+  return win || /** @type {!Window} */ (goog.global);
+};
diff --git a/third_party/ink/closure/dom/browserfeature.js b/third_party/ink/closure/dom/browserfeature.js
new file mode 100644
index 0000000..2d418ea
--- /dev/null
+++ b/third_party/ink/closure/dom/browserfeature.js
@@ -0,0 +1,74 @@
+// Copyright 2010 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 Browser capability checks for the dom package.
+ *
+ * @author zhyder@google.com (Zohair Hyder)
+ */
+
+
+goog.provide('goog.dom.BrowserFeature');
+
+goog.require('goog.userAgent');
+
+
+/**
+ * Enum of browser capabilities.
+ * @enum {boolean}
+ */
+goog.dom.BrowserFeature = {
+  /**
+   * Whether attributes 'name' and 'type' can be added to an element after it's
+   * created. False in Internet Explorer prior to version 9.
+   */
+  CAN_ADD_NAME_OR_TYPE_ATTRIBUTES:
+      !goog.userAgent.IE || goog.userAgent.isDocumentModeOrHigher(9),
+
+  /**
+   * Whether we can use element.children to access an element's Element
+   * children. Available since Gecko 1.9.1, IE 9. (IE<9 also includes comment
+   * nodes in the collection.)
+   */
+  CAN_USE_CHILDREN_ATTRIBUTE: !goog.userAgent.GECKO && !goog.userAgent.IE ||
+      goog.userAgent.IE && goog.userAgent.isDocumentModeOrHigher(9) ||
+      goog.userAgent.GECKO && goog.userAgent.isVersionOrHigher('1.9.1'),
+
+  /**
+   * Opera, Safari 3, and Internet Explorer 9 all support innerText but they
+   * include text nodes in script and style tags. Not document-mode-dependent.
+   */
+  CAN_USE_INNER_TEXT:
+      (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9')),
+
+  /**
+   * MSIE, Opera, and Safari>=4 support element.parentElement to access an
+   * element's parent if it is an Element.
+   */
+  CAN_USE_PARENT_ELEMENT_PROPERTY:
+      goog.userAgent.IE || goog.userAgent.OPERA || goog.userAgent.WEBKIT,
+
+  /**
+   * Whether NoScope elements need a scoped element written before them in
+   * innerHTML.
+   * MSDN: http://msdn.microsoft.com/en-us/library/ms533897(VS.85).aspx#1
+   */
+  INNER_HTML_NEEDS_SCOPED_ELEMENT: goog.userAgent.IE,
+
+  /**
+   * Whether we use legacy IE range API.
+   */
+  LEGACY_IE_RANGES:
+      goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)
+};
diff --git a/third_party/ink/closure/dom/classlist.js b/third_party/ink/closure/dom/classlist.js
new file mode 100644
index 0000000..0d7afbf
--- /dev/null
+++ b/third_party/ink/closure/dom/classlist.js
@@ -0,0 +1,276 @@
+// Copyright 2012 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 Utilities for detecting, adding and removing classes.  Prefer
+ * this over goog.dom.classes for new code since it attempts to use classList
+ * (DOMTokenList: http://dom.spec.whatwg.org/#domtokenlist) which is faster
+ * and requires less code.
+ *
+ * Note: these utilities are meant to operate on HTMLElements
+ * and may have unexpected behavior on elements with differing interfaces
+ * (such as SVGElements).
+ */
+
+
+goog.provide('goog.dom.classlist');
+
+goog.require('goog.array');
+
+
+/**
+ * Override this define at build-time if you know your target supports it.
+ * @define {boolean} Whether to use the classList property (DOMTokenList).
+ */
+goog.define('goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST', false);
+
+
+/**
+ * Gets an array-like object of class names on an element.
+ * @param {Element} element DOM node to get the classes of.
+ * @return {!IArrayLike<?>} Class names on {@code element}.
+ */
+goog.dom.classlist.get = function(element) {
+  if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
+    return element.classList;
+  }
+
+  var className = element.className;
+  // Some types of elements don't have a className in IE (e.g. iframes).
+  // Furthermore, in Firefox, className is not a string when the element is
+  // an SVG element.
+  return goog.isString(className) && className.match(/\S+/g) || [];
+};
+
+
+/**
+ * Sets the entire class name of an element.
+ * @param {Element} element DOM node to set class of.
+ * @param {string} className Class name(s) to apply to element.
+ */
+goog.dom.classlist.set = function(element, className) {
+  element.className = className;
+};
+
+
+/**
+ * Returns true if an element has a class.  This method may throw a DOM
+ * exception for an invalid or empty class name if DOMTokenList is used.
+ * @param {Element} element DOM node to test.
+ * @param {string} className Class name to test for.
+ * @return {boolean} Whether element has the class.
+ */
+goog.dom.classlist.contains = function(element, className) {
+  if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
+    return element.classList.contains(className);
+  }
+  return goog.array.contains(goog.dom.classlist.get(element), className);
+};
+
+
+/**
+ * Adds a class to an element.  Does not add multiples of class names.  This
+ * method may throw a DOM exception for an invalid or empty class name if
+ * DOMTokenList is used.
+ * @param {Element} element DOM node to add class to.
+ * @param {string} className Class name to add.
+ */
+goog.dom.classlist.add = function(element, className) {
+  if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
+    element.classList.add(className);
+    return;
+  }
+
+  if (!goog.dom.classlist.contains(element, className)) {
+    // Ensure we add a space if this is not the first class name added.
+    element.className +=
+        element.className.length > 0 ? (' ' + className) : className;
+  }
+};
+
+
+/**
+ * Convenience method to add a number of class names at once.
+ * @param {Element} element The element to which to add classes.
+ * @param {IArrayLike<string>} classesToAdd An array-like object
+ * containing a collection of class names to add to the element.
+ * This method may throw a DOM exception if classesToAdd contains invalid
+ * or empty class names.
+ */
+goog.dom.classlist.addAll = function(element, classesToAdd) {
+  if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
+    goog.array.forEach(classesToAdd, function(className) {
+      goog.dom.classlist.add(element, className);
+    });
+    return;
+  }
+
+  var classMap = {};
+
+  // Get all current class names into a map.
+  goog.array.forEach(goog.dom.classlist.get(element), function(className) {
+    classMap[className] = true;
+  });
+
+  // Add new class names to the map.
+  goog.array.forEach(
+      classesToAdd, function(className) { classMap[className] = true; });
+
+  // Flatten the keys of the map into the className.
+  element.className = '';
+  for (var className in classMap) {
+    element.className +=
+        element.className.length > 0 ? (' ' + className) : className;
+  }
+};
+
+
+/**
+ * Removes a class from an element.  This method may throw a DOM exception
+ * for an invalid or empty class name if DOMTokenList is used.
+ * @param {Element} element DOM node to remove class from.
+ * @param {string} className Class name to remove.
+ */
+goog.dom.classlist.remove = function(element, className) {
+  if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
+    element.classList.remove(className);
+    return;
+  }
+
+  if (goog.dom.classlist.contains(element, className)) {
+    // Filter out the class name.
+    element.className = goog.array
+                            .filter(
+                                goog.dom.classlist.get(element),
+                                function(c) { return c != className; })
+                            .join(' ');
+  }
+};
+
+
+/**
+ * Removes a set of classes from an element.  Prefer this call to
+ * repeatedly calling {@code goog.dom.classlist.remove} if you want to remove
+ * a large set of class names at once.
+ * @param {Element} element The element from which to remove classes.
+ * @param {IArrayLike<string>} classesToRemove An array-like object
+ * containing a collection of class names to remove from the element.
+ * This method may throw a DOM exception if classesToRemove contains invalid
+ * or empty class names.
+ */
+goog.dom.classlist.removeAll = function(element, classesToRemove) {
+  if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
+    goog.array.forEach(classesToRemove, function(className) {
+      goog.dom.classlist.remove(element, className);
+    });
+    return;
+  }
+  // Filter out those classes in classesToRemove.
+  element.className =
+      goog.array
+          .filter(
+              goog.dom.classlist.get(element),
+              function(className) {
+                // If this class is not one we are trying to remove,
+                // add it to the array of new class names.
+                return !goog.array.contains(classesToRemove, className);
+              })
+          .join(' ');
+};
+
+
+/**
+ * Adds or removes a class depending on the enabled argument.  This method
+ * may throw a DOM exception for an invalid or empty class name if DOMTokenList
+ * is used.
+ * @param {Element} element DOM node to add or remove the class on.
+ * @param {string} className Class name to add or remove.
+ * @param {boolean} enabled Whether to add or remove the class (true adds,
+ *     false removes).
+ */
+goog.dom.classlist.enable = function(element, className, enabled) {
+  if (enabled) {
+    goog.dom.classlist.add(element, className);
+  } else {
+    goog.dom.classlist.remove(element, className);
+  }
+};
+
+
+/**
+ * Adds or removes a set of classes depending on the enabled argument.  This
+ * method may throw a DOM exception for an invalid or empty class name if
+ * DOMTokenList is used.
+ * @param {!Element} element DOM node to add or remove the class on.
+ * @param {?IArrayLike<string>} classesToEnable An array-like object
+ *     containing a collection of class names to add or remove from the element.
+ * @param {boolean} enabled Whether to add or remove the classes (true adds,
+ *     false removes).
+ */
+goog.dom.classlist.enableAll = function(element, classesToEnable, enabled) {
+  var f = enabled ? goog.dom.classlist.addAll : goog.dom.classlist.removeAll;
+  f(element, classesToEnable);
+};
+
+
+/**
+ * Switches a class on an element from one to another without disturbing other
+ * classes. If the fromClass isn't removed, the toClass won't be added.  This
+ * method may throw a DOM exception if the class names are empty or invalid.
+ * @param {Element} element DOM node to swap classes on.
+ * @param {string} fromClass Class to remove.
+ * @param {string} toClass Class to add.
+ * @return {boolean} Whether classes were switched.
+ */
+goog.dom.classlist.swap = function(element, fromClass, toClass) {
+  if (goog.dom.classlist.contains(element, fromClass)) {
+    goog.dom.classlist.remove(element, fromClass);
+    goog.dom.classlist.add(element, toClass);
+    return true;
+  }
+  return false;
+};
+
+
+/**
+ * Removes a class if an element has it, and adds it the element doesn't have
+ * it.  Won't affect other classes on the node.  This method may throw a DOM
+ * exception if the class name is empty or invalid.
+ * @param {Element} element DOM node to toggle class on.
+ * @param {string} className Class to toggle.
+ * @return {boolean} True if class was added, false if it was removed
+ *     (in other words, whether element has the class after this function has
+ *     been called).
+ */
+goog.dom.classlist.toggle = function(element, className) {
+  var add = !goog.dom.classlist.contains(element, className);
+  goog.dom.classlist.enable(element, className, add);
+  return add;
+};
+
+
+/**
+ * Adds and removes a class of an element.  Unlike
+ * {@link goog.dom.classlist.swap}, this method adds the classToAdd regardless
+ * of whether the classToRemove was present and had been removed.  This method
+ * may throw a DOM exception if the class names are empty or invalid.
+ *
+ * @param {Element} element DOM node to swap classes on.
+ * @param {string} classToRemove Class to remove.
+ * @param {string} classToAdd Class to add.
+ */
+goog.dom.classlist.addRemove = function(element, classToRemove, classToAdd) {
+  goog.dom.classlist.remove(element, classToRemove);
+  goog.dom.classlist.add(element, classToAdd);
+};
diff --git a/third_party/ink/closure/dom/dom.js b/third_party/ink/closure/dom/dom.js
new file mode 100644
index 0000000..a330b4c
--- /dev/null
+++ b/third_party/ink/closure/dom/dom.js
@@ -0,0 +1,3234 @@
+// Copyright 2006 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 Utilities for manipulating the browser's Document Object Model
+ * Inspiration taken *heavily* from mochikit (http://mochikit.com/).
+ *
+ * You can use {@link goog.dom.DomHelper} to create new dom helpers that refer
+ * to a different document object.  This is useful if you are working with
+ * frames or multiple windows.
+ *
+ * @author pupius@google.com (Daniel Pupius)
+ * @author arv@google.com (Erik Arvidsson)
+ */
+
+
+// TODO(arv): Rename/refactor getTextContent and getRawTextContent. The problem
+// is that getTextContent should mimic the DOM3 textContent. We should add a
+// getInnerText (or getText) which tries to return the visible text, innerText.
+
+
+goog.provide('goog.dom');
+goog.provide('goog.dom.Appendable');
+goog.provide('goog.dom.DomHelper');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.dom.BrowserFeature');
+goog.require('goog.dom.NodeType');
+goog.require('goog.dom.TagName');
+goog.require('goog.dom.safe');
+goog.require('goog.html.SafeHtml');
+goog.require('goog.html.uncheckedconversions');
+goog.require('goog.math.Coordinate');
+goog.require('goog.math.Size');
+goog.require('goog.object');
+goog.require('goog.string');
+goog.require('goog.string.Unicode');
+goog.require('goog.userAgent');
+
+
+/**
+ * @define {boolean} Whether we know at compile time that the browser is in
+ * quirks mode.
+ */
+goog.define('goog.dom.ASSUME_QUIRKS_MODE', false);
+
+
+/**
+ * @define {boolean} Whether we know at compile time that the browser is in
+ * standards compliance mode.
+ */
+goog.define('goog.dom.ASSUME_STANDARDS_MODE', false);
+
+
+/**
+ * Whether we know the compatibility mode at compile time.
+ * @type {boolean}
+ * @private
+ */
+goog.dom.COMPAT_MODE_KNOWN_ =
+    goog.dom.ASSUME_QUIRKS_MODE || goog.dom.ASSUME_STANDARDS_MODE;
+
+
+/**
+ * Gets the DomHelper object for the document where the element resides.
+ * @param {(Node|Window)=} opt_element If present, gets the DomHelper for this
+ *     element.
+ * @return {!goog.dom.DomHelper} The DomHelper.
+ */
+goog.dom.getDomHelper = function(opt_element) {
+  return opt_element ?
+      new goog.dom.DomHelper(goog.dom.getOwnerDocument(opt_element)) :
+      (goog.dom.defaultDomHelper_ ||
+       (goog.dom.defaultDomHelper_ = new goog.dom.DomHelper()));
+};
+
+
+/**
+ * Cached default DOM helper.
+ * @type {!goog.dom.DomHelper|undefined}
+ * @private
+ */
+goog.dom.defaultDomHelper_;
+
+
+/**
+ * Gets the document object being used by the dom library.
+ * @return {!Document} Document object.
+ */
+goog.dom.getDocument = function() {
+  return document;
+};
+
+
+/**
+ * Gets an element from the current document by element id.
+ *
+ * If an Element is passed in, it is returned.
+ *
+ * @param {string|Element} element Element ID or a DOM node.
+ * @return {Element} The element with the given ID, or the node passed in.
+ */
+goog.dom.getElement = function(element) {
+  return goog.dom.getElementHelper_(document, element);
+};
+
+
+/**
+ * Gets an element by id from the given document (if present).
+ * If an element is given, it is returned.
+ * @param {!Document} doc
+ * @param {string|Element} element Element ID or a DOM node.
+ * @return {Element} The resulting element.
+ * @private
+ */
+goog.dom.getElementHelper_ = function(doc, element) {
+  return goog.isString(element) ? doc.getElementById(element) : element;
+};
+
+
+/**
+ * Gets an element by id, asserting that the element is found.
+ *
+ * This is used when an element is expected to exist, and should fail with
+ * an assertion error if it does not (if assertions are enabled).
+ *
+ * @param {string} id Element ID.
+ * @return {!Element} The element with the given ID, if it exists.
+ */
+goog.dom.getRequiredElement = function(id) {
+  return goog.dom.getRequiredElementHelper_(document, id);
+};
+
+
+/**
+ * Helper function for getRequiredElementHelper functions, both static and
+ * on DomHelper.  Asserts the element with the given id exists.
+ * @param {!Document} doc
+ * @param {string} id
+ * @return {!Element} The element with the given ID, if it exists.
+ * @private
+ */
+goog.dom.getRequiredElementHelper_ = function(doc, id) {
+  // To prevent users passing in Elements as is permitted in getElement().
+  goog.asserts.assertString(id);
+  var element = goog.dom.getElementHelper_(doc, id);
+  element =
+      goog.asserts.assertElement(element, 'No element found with id: ' + id);
+  return element;
+};
+
+
+/**
+ * Alias for getElement.
+ * @param {string|Element} element Element ID or a DOM node.
+ * @return {Element} The element with the given ID, or the node passed in.
+ * @deprecated Use {@link goog.dom.getElement} instead.
+ */
+goog.dom.$ = goog.dom.getElement;
+
+
+/**
+ * Gets elements by tag name.
+ * @param {!goog.dom.TagName<T>} tagName
+ * @param {(!Document|!Element)=} opt_parent Parent element or document where to
+ *     look for elements. Defaults to document.
+ * @return {!NodeList<R>} List of elements. The members of the list are
+ *     {!Element} if tagName is not a member of goog.dom.TagName or more
+ *     specific types if it is (e.g. {!HTMLAnchorElement} for
+ *     goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ */
+goog.dom.getElementsByTagName = function(tagName, opt_parent) {
+  var parent = opt_parent || document;
+  return parent.getElementsByTagName(String(tagName));
+};
+
+
+/**
+ * Looks up elements by both tag and class name, using browser native functions
+ * ({@code querySelectorAll}, {@code getElementsByTagName} or
+ * {@code getElementsByClassName}) where possible. This function
+ * is a useful, if limited, way of collecting a list of DOM elements
+ * with certain characteristics.  {@code goog.dom.query} offers a
+ * more powerful and general solution which allows matching on CSS3
+ * selector expressions, but at increased cost in code size. If all you
+ * need is particular tags belonging to a single class, this function
+ * is fast and sleek.
+ *
+ * Note that tag names are case sensitive in the SVG namespace, and this
+ * function converts opt_tag to uppercase for comparisons. For queries in the
+ * SVG namespace you should use querySelector or querySelectorAll instead.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=963870
+ * https://bugs.webkit.org/show_bug.cgi?id=83438
+ *
+ * @see {goog.dom.query}
+ *
+ * @param {(string|?goog.dom.TagName<T>)=} opt_tag Element tag name.
+ * @param {?string=} opt_class Optional class name.
+ * @param {(Document|Element)=} opt_el Optional element to look in.
+ * @return {!IArrayLike<R>} Array-like list of elements (only a length property
+ *     and numerical indices are guaranteed to exist). The members of the array
+ *     are {!Element} if opt_tag is not a member of goog.dom.TagName or more
+ *     specific types if it is (e.g. {!HTMLAnchorElement} for
+ *     goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ */
+goog.dom.getElementsByTagNameAndClass = function(opt_tag, opt_class, opt_el) {
+  return goog.dom.getElementsByTagNameAndClass_(
+      document, opt_tag, opt_class, opt_el);
+};
+
+
+/**
+ * Gets the first element matching the tag and the class.
+ *
+ * @param {(string|?goog.dom.TagName<T>)=} opt_tag Element tag name.
+ * @param {?string=} opt_class Optional class name.
+ * @param {(Document|Element)=} opt_el Optional element to look in.
+ * @return {?R} Reference to a DOM node. The return type is {?Element} if
+ *     tagName is a string or a more specific type if it is a member of
+ *     goog.dom.TagName (e.g. {?HTMLAnchorElement} for goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ */
+goog.dom.getElementByTagNameAndClass = function(opt_tag, opt_class, opt_el) {
+  return goog.dom.getElementByTagNameAndClass_(
+      document, opt_tag, opt_class, opt_el);
+};
+
+
+/**
+ * Returns a static, array-like list of the elements with the provided
+ * className.
+ * @see {goog.dom.query}
+ * @param {string} className the name of the class to look for.
+ * @param {(Document|Element)=} opt_el Optional element to look in.
+ * @return {!IArrayLike<!Element>} The items found with the class name provided.
+ */
+goog.dom.getElementsByClass = function(className, opt_el) {
+  var parent = opt_el || document;
+  if (goog.dom.canUseQuerySelector_(parent)) {
+    return parent.querySelectorAll('.' + className);
+  }
+  return goog.dom.getElementsByTagNameAndClass_(
+      document, '*', className, opt_el);
+};
+
+
+/**
+ * Returns the first element with the provided className.
+ * @see {goog.dom.query}
+ * @param {string} className the name of the class to look for.
+ * @param {Element|Document=} opt_el Optional element to look in.
+ * @return {Element} The first item with the class name provided.
+ */
+goog.dom.getElementByClass = function(className, opt_el) {
+  var parent = opt_el || document;
+  var retVal = null;
+  if (parent.getElementsByClassName) {
+    retVal = parent.getElementsByClassName(className)[0];
+  } else {
+    retVal =
+        goog.dom.getElementByTagNameAndClass_(document, '*', className, opt_el);
+  }
+  return retVal || null;
+};
+
+
+/**
+ * Ensures an element with the given className exists, and then returns the
+ * first element with the provided className.
+ * @see {goog.dom.query}
+ * @param {string} className the name of the class to look for.
+ * @param {!Element|!Document=} opt_root Optional element or document to look
+ *     in.
+ * @return {!Element} The first item with the class name provided.
+ * @throws {goog.asserts.AssertionError} Thrown if no element is found.
+ */
+goog.dom.getRequiredElementByClass = function(className, opt_root) {
+  var retValue = goog.dom.getElementByClass(className, opt_root);
+  return goog.asserts.assert(
+      retValue, 'No element found with className: ' + className);
+};
+
+
+/**
+ * Prefer the standardized (http://www.w3.org/TR/selectors-api/), native and
+ * fast W3C Selectors API.
+ * @param {!(Element|Document)} parent The parent document object.
+ * @return {boolean} whether or not we can use parent.querySelector* APIs.
+ * @private
+ */
+goog.dom.canUseQuerySelector_ = function(parent) {
+  return !!(parent.querySelectorAll && parent.querySelector);
+};
+
+
+/**
+ * Helper for {@code getElementsByTagNameAndClass}.
+ * @param {!Document} doc The document to get the elements in.
+ * @param {(string|?goog.dom.TagName<T>)=} opt_tag Element tag name.
+ * @param {?string=} opt_class Optional class name.
+ * @param {(Document|Element)=} opt_el Optional element to look in.
+ * @return {!IArrayLike<R>} Array-like list of elements (only a length property
+ *     and numerical indices are guaranteed to exist). The members of the array
+ *     are {!Element} if opt_tag is not a member of goog.dom.TagName or more
+ *     specific types if it is (e.g. {!HTMLAnchorElement} for
+ *     goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ * @private
+ */
+goog.dom.getElementsByTagNameAndClass_ = function(
+    doc, opt_tag, opt_class, opt_el) {
+  var parent = opt_el || doc;
+  var tagName =
+      (opt_tag && opt_tag != '*') ? String(opt_tag).toUpperCase() : '';
+
+  if (goog.dom.canUseQuerySelector_(parent) && (tagName || opt_class)) {
+    var query = tagName + (opt_class ? '.' + opt_class : '');
+    return parent.querySelectorAll(query);
+  }
+
+  // Use the native getElementsByClassName if available, under the assumption
+  // that even when the tag name is specified, there will be fewer elements to
+  // filter through when going by class than by tag name
+  if (opt_class && parent.getElementsByClassName) {
+    var els = parent.getElementsByClassName(opt_class);
+
+    if (tagName) {
+      var arrayLike = {};
+      var len = 0;
+
+      // Filter for specific tags if requested.
+      for (var i = 0, el; el = els[i]; i++) {
+        if (tagName == el.nodeName) {
+          arrayLike[len++] = el;
+        }
+      }
+      arrayLike.length = len;
+
+      return /** @type {!IArrayLike<!Element>} */ (arrayLike);
+    } else {
+      return els;
+    }
+  }
+
+  var els = parent.getElementsByTagName(tagName || '*');
+
+  if (opt_class) {
+    var arrayLike = {};
+    var len = 0;
+    for (var i = 0, el; el = els[i]; i++) {
+      var className = el.className;
+      // Check if className has a split function since SVG className does not.
+      if (typeof className.split == 'function' &&
+          goog.array.contains(className.split(/\s+/), opt_class)) {
+        arrayLike[len++] = el;
+      }
+    }
+    arrayLike.length = len;
+    return /** @type {!IArrayLike<!Element>} */ (arrayLike);
+  } else {
+    return els;
+  }
+};
+
+
+/**
+ * Helper for goog.dom.getElementByTagNameAndClass.
+ *
+ * @param {!Document} doc The document to get the elements in.
+ * @param {(string|?goog.dom.TagName<T>)=} opt_tag Element tag name.
+ * @param {?string=} opt_class Optional class name.
+ * @param {(Document|Element)=} opt_el Optional element to look in.
+ * @return {?R} Reference to a DOM node. The return type is {?Element} if
+ *     tagName is a string or a more specific type if it is a member of
+ *     goog.dom.TagName (e.g. {?HTMLAnchorElement} for goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ * @private
+ */
+goog.dom.getElementByTagNameAndClass_ = function(
+    doc, opt_tag, opt_class, opt_el) {
+  var parent = opt_el || doc;
+  var tag = (opt_tag && opt_tag != '*') ? String(opt_tag).toUpperCase() : '';
+  if (goog.dom.canUseQuerySelector_(parent) && (tag || opt_class)) {
+    return parent.querySelector(tag + (opt_class ? '.' + opt_class : ''));
+  }
+  var elements =
+      goog.dom.getElementsByTagNameAndClass_(doc, opt_tag, opt_class, opt_el);
+  return elements[0] || null;
+};
+
+
+
+/**
+ * Alias for {@code getElementsByTagNameAndClass}.
+ * @param {(string|?goog.dom.TagName<T>)=} opt_tag Element tag name.
+ * @param {?string=} opt_class Optional class name.
+ * @param {Element=} opt_el Optional element to look in.
+ * @return {!IArrayLike<R>} Array-like list of elements (only a length property
+ *     and numerical indices are guaranteed to exist). The members of the array
+ *     are {!Element} if opt_tag is not a member of goog.dom.TagName or more
+ *     specific types if it is (e.g. {!HTMLAnchorElement} for
+ *     goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ * @deprecated Use {@link goog.dom.getElementsByTagNameAndClass} instead.
+ */
+goog.dom.$$ = goog.dom.getElementsByTagNameAndClass;
+
+
+/**
+ * Sets multiple properties, and sometimes attributes, on an element. Note that
+ * properties are simply object properties on the element instance, while
+ * attributes are visible in the DOM. Many properties map to attributes with the
+ * same names, some with different names, and there are also unmappable cases.
+ *
+ * This method sets properties by default (which means that custom attributes
+ * are not supported). These are the exeptions (some of which is legacy):
+ * - "style": Even though this is an attribute name, it is translated to a
+ *   property, "style.cssText". Note that this property sanitizes and formats
+ *   its value, unlike the attribute.
+ * - "class": This is an attribute name, it is translated to the "className"
+ *   property.
+ * - "for": This is an attribute name, it is translated to the "htmlFor"
+ *   property.
+ * - Entries in {@see goog.dom.DIRECT_ATTRIBUTE_MAP_} are set as attributes,
+ *   this is probably due to browser quirks.
+ * - "aria-*", "data-*": Always set as attributes, they have no property
+ *   counterparts.
+ *
+ * @param {Element} element DOM node to set properties on.
+ * @param {Object} properties Hash of property:value pairs.
+ *     Property values can be strings or goog.string.TypedString values (such as
+ *     goog.html.SafeUrl).
+ */
+goog.dom.setProperties = function(element, properties) {
+  goog.object.forEach(properties, function(val, key) {
+    if (val && val.implementsGoogStringTypedString) {
+      val = val.getTypedStringValue();
+    }
+    if (key == 'style') {
+      element.style.cssText = val;
+    } else if (key == 'class') {
+      element.className = val;
+    } else if (key == 'for') {
+      element.htmlFor = val;
+    } else if (goog.dom.DIRECT_ATTRIBUTE_MAP_.hasOwnProperty(key)) {
+      element.setAttribute(goog.dom.DIRECT_ATTRIBUTE_MAP_[key], val);
+    } else if (
+        goog.string.startsWith(key, 'aria-') ||
+        goog.string.startsWith(key, 'data-')) {
+      element.setAttribute(key, val);
+    } else {
+      element[key] = val;
+    }
+  });
+};
+
+
+/**
+ * Map of attributes that should be set using
+ * element.setAttribute(key, val) instead of element[key] = val.  Used
+ * by goog.dom.setProperties.
+ *
+ * @private {!Object<string, string>}
+ * @const
+ */
+goog.dom.DIRECT_ATTRIBUTE_MAP_ = {
+  'cellpadding': 'cellPadding',
+  'cellspacing': 'cellSpacing',
+  'colspan': 'colSpan',
+  'frameborder': 'frameBorder',
+  'height': 'height',
+  'maxlength': 'maxLength',
+  'nonce': 'nonce',
+  'role': 'role',
+  'rowspan': 'rowSpan',
+  'type': 'type',
+  'usemap': 'useMap',
+  'valign': 'vAlign',
+  'width': 'width'
+};
+
+
+/**
+ * Gets the dimensions of the viewport.
+ *
+ * Gecko Standards mode:
+ * docEl.clientWidth  Width of viewport excluding scrollbar.
+ * win.innerWidth     Width of viewport including scrollbar.
+ * body.clientWidth   Width of body element.
+ *
+ * docEl.clientHeight Height of viewport excluding scrollbar.
+ * win.innerHeight    Height of viewport including scrollbar.
+ * body.clientHeight  Height of document.
+ *
+ * Gecko Backwards compatible mode:
+ * docEl.clientWidth  Width of viewport excluding scrollbar.
+ * win.innerWidth     Width of viewport including scrollbar.
+ * body.clientWidth   Width of viewport excluding scrollbar.
+ *
+ * docEl.clientHeight Height of document.
+ * win.innerHeight    Height of viewport including scrollbar.
+ * body.clientHeight  Height of viewport excluding scrollbar.
+ *
+ * IE6/7 Standards mode:
+ * docEl.clientWidth  Width of viewport excluding scrollbar.
+ * win.innerWidth     Undefined.
+ * body.clientWidth   Width of body element.
+ *
+ * docEl.clientHeight Height of viewport excluding scrollbar.
+ * win.innerHeight    Undefined.
+ * body.clientHeight  Height of document element.
+ *
+ * IE5 + IE6/7 Backwards compatible mode:
+ * docEl.clientWidth  0.
+ * win.innerWidth     Undefined.
+ * body.clientWidth   Width of viewport excluding scrollbar.
+ *
+ * docEl.clientHeight 0.
+ * win.innerHeight    Undefined.
+ * body.clientHeight  Height of viewport excluding scrollbar.
+ *
+ * Opera 9 Standards and backwards compatible mode:
+ * docEl.clientWidth  Width of viewport excluding scrollbar.
+ * win.innerWidth     Width of viewport including scrollbar.
+ * body.clientWidth   Width of viewport excluding scrollbar.
+ *
+ * docEl.clientHeight Height of document.
+ * win.innerHeight    Height of viewport including scrollbar.
+ * body.clientHeight  Height of viewport excluding scrollbar.
+ *
+ * WebKit:
+ * Safari 2
+ * docEl.clientHeight Same as scrollHeight.
+ * docEl.clientWidth  Same as innerWidth.
+ * win.innerWidth     Width of viewport excluding scrollbar.
+ * win.innerHeight    Height of the viewport including scrollbar.
+ * frame.innerHeight  Height of the viewport exluding scrollbar.
+ *
+ * Safari 3 (tested in 522)
+ *
+ * docEl.clientWidth  Width of viewport excluding scrollbar.
+ * docEl.clientHeight Height of viewport excluding scrollbar in strict mode.
+ * body.clientHeight  Height of viewport excluding scrollbar in quirks mode.
+ *
+ * @param {Window=} opt_window Optional window element to test.
+ * @return {!goog.math.Size} Object with values 'width' and 'height'.
+ */
+goog.dom.getViewportSize = function(opt_window) {
+  // TODO(arv): This should not take an argument
+  return goog.dom.getViewportSize_(opt_window || window);
+};
+
+
+/**
+ * Helper for {@code getViewportSize}.
+ * @param {Window} win The window to get the view port size for.
+ * @return {!goog.math.Size} Object with values 'width' and 'height'.
+ * @private
+ */
+goog.dom.getViewportSize_ = function(win) {
+  var doc = win.document;
+  var el = goog.dom.isCss1CompatMode_(doc) ? doc.documentElement : doc.body;
+  return new goog.math.Size(el.clientWidth, el.clientHeight);
+};
+
+
+/**
+ * Calculates the height of the document.
+ *
+ * @return {number} The height of the current document.
+ */
+goog.dom.getDocumentHeight = function() {
+  return goog.dom.getDocumentHeight_(window);
+};
+
+/**
+ * Calculates the height of the document of the given window.
+ *
+ * @param {!Window} win The window whose document height to retrieve.
+ * @return {number} The height of the document of the given window.
+ */
+goog.dom.getDocumentHeightForWindow = function(win) {
+  return goog.dom.getDocumentHeight_(win);
+};
+
+/**
+ * Calculates the height of the document of the given window.
+ *
+ * Function code copied from the opensocial gadget api:
+ *   gadgets.window.adjustHeight(opt_height)
+ *
+ * @private
+ * @param {!Window} win The window whose document height to retrieve.
+ * @return {number} The height of the document of the given window.
+ */
+goog.dom.getDocumentHeight_ = function(win) {
+  // NOTE(eae): This method will return the window size rather than the document
+  // size in webkit quirks mode.
+  var doc = win.document;
+  var height = 0;
+
+  if (doc) {
+    // Calculating inner content height is hard and different between
+    // browsers rendering in Strict vs. Quirks mode.  We use a combination of
+    // three properties within document.body and document.documentElement:
+    // - scrollHeight
+    // - offsetHeight
+    // - clientHeight
+    // These values differ significantly between browsers and rendering modes.
+    // But there are patterns.  It just takes a lot of time and persistence
+    // to figure out.
+
+    var body = doc.body;
+    var docEl = /** @type {!HTMLElement} */ (doc.documentElement);
+    if (!(docEl && body)) {
+      return 0;
+    }
+
+    // Get the height of the viewport
+    var vh = goog.dom.getViewportSize_(win).height;
+    if (goog.dom.isCss1CompatMode_(doc) && docEl.scrollHeight) {
+      // In Strict mode:
+      // The inner content height is contained in either:
+      //    document.documentElement.scrollHeight
+      //    document.documentElement.offsetHeight
+      // Based on studying the values output by different browsers,
+      // use the value that's NOT equal to the viewport height found above.
+      height =
+          docEl.scrollHeight != vh ? docEl.scrollHeight : docEl.offsetHeight;
+    } else {
+      // In Quirks mode:
+      // documentElement.clientHeight is equal to documentElement.offsetHeight
+      // except in IE.  In most browsers, document.documentElement can be used
+      // to calculate the inner content height.
+      // However, in other browsers (e.g. IE), document.body must be used
+      // instead.  How do we know which one to use?
+      // If document.documentElement.clientHeight does NOT equal
+      // document.documentElement.offsetHeight, then use document.body.
+      var sh = docEl.scrollHeight;
+      var oh = docEl.offsetHeight;
+      if (docEl.clientHeight != oh) {
+        sh = body.scrollHeight;
+        oh = body.offsetHeight;
+      }
+
+      // Detect whether the inner content height is bigger or smaller
+      // than the bounding box (viewport).  If bigger, take the larger
+      // value.  If smaller, take the smaller value.
+      if (sh > vh) {
+        // Content is larger
+        height = sh > oh ? sh : oh;
+      } else {
+        // Content is smaller
+        height = sh < oh ? sh : oh;
+      }
+    }
+  }
+
+  return height;
+};
+
+
+/**
+ * Gets the page scroll distance as a coordinate object.
+ *
+ * @param {Window=} opt_window Optional window element to test.
+ * @return {!goog.math.Coordinate} Object with values 'x' and 'y'.
+ * @deprecated Use {@link goog.dom.getDocumentScroll} instead.
+ */
+goog.dom.getPageScroll = function(opt_window) {
+  var win = opt_window || goog.global || window;
+  return goog.dom.getDomHelper(win.document).getDocumentScroll();
+};
+
+
+/**
+ * Gets the document scroll distance as a coordinate object.
+ *
+ * @return {!goog.math.Coordinate} Object with values 'x' and 'y'.
+ */
+goog.dom.getDocumentScroll = function() {
+  return goog.dom.getDocumentScroll_(document);
+};
+
+
+/**
+ * Helper for {@code getDocumentScroll}.
+ *
+ * @param {!Document} doc The document to get the scroll for.
+ * @return {!goog.math.Coordinate} Object with values 'x' and 'y'.
+ * @private
+ */
+goog.dom.getDocumentScroll_ = function(doc) {
+  var el = goog.dom.getDocumentScrollElement_(doc);
+  var win = goog.dom.getWindow_(doc);
+  if (goog.userAgent.IE && goog.userAgent.isVersionOrHigher('10') &&
+      win.pageYOffset != el.scrollTop) {
+    // The keyboard on IE10 touch devices shifts the page using the pageYOffset
+    // without modifying scrollTop. For this case, we want the body scroll
+    // offsets.
+    return new goog.math.Coordinate(el.scrollLeft, el.scrollTop);
+  }
+  return new goog.math.Coordinate(
+      win.pageXOffset || el.scrollLeft, win.pageYOffset || el.scrollTop);
+};
+
+
+/**
+ * Gets the document scroll element.
+ * @return {!Element} Scrolling element.
+ */
+goog.dom.getDocumentScrollElement = function() {
+  return goog.dom.getDocumentScrollElement_(document);
+};
+
+
+/**
+ * Helper for {@code getDocumentScrollElement}.
+ * @param {!Document} doc The document to get the scroll element for.
+ * @return {!Element} Scrolling element.
+ * @private
+ */
+goog.dom.getDocumentScrollElement_ = function(doc) {
+  // Old WebKit needs body.scrollLeft in both quirks mode and strict mode. We
+  // also default to the documentElement if the document does not have a body
+  // (e.g. a SVG document).
+  // Uses http://dev.w3.org/csswg/cssom-view/#dom-document-scrollingelement to
+  // avoid trying to guess about browser behavior from the UA string.
+  if (doc.scrollingElement) {
+    return doc.scrollingElement;
+  }
+  if (!goog.userAgent.WEBKIT && goog.dom.isCss1CompatMode_(doc)) {
+    return doc.documentElement;
+  }
+  return doc.body || doc.documentElement;
+};
+
+
+/**
+ * Gets the window object associated with the given document.
+ *
+ * @param {Document=} opt_doc  Document object to get window for.
+ * @return {!Window} The window associated with the given document.
+ */
+goog.dom.getWindow = function(opt_doc) {
+  // TODO(arv): This should not take an argument.
+  return opt_doc ? goog.dom.getWindow_(opt_doc) : window;
+};
+
+
+/**
+ * Helper for {@code getWindow}.
+ *
+ * @param {!Document} doc  Document object to get window for.
+ * @return {!Window} The window associated with the given document.
+ * @private
+ */
+goog.dom.getWindow_ = function(doc) {
+  return /** @type {!Window} */ (doc.parentWindow || doc.defaultView);
+};
+
+
+/**
+ * Returns a dom node with a set of attributes.  This function accepts varargs
+ * for subsequent nodes to be added.  Subsequent nodes will be added to the
+ * first node as childNodes.
+ *
+ * So:
+ * <code>createDom(goog.dom.TagName.DIV, null, createDom(goog.dom.TagName.P),
+ * createDom(goog.dom.TagName.P));</code> would return a div with two child
+ * paragraphs
+ *
+ * For passing properties, please see {@link goog.dom.setProperties} for more
+ * information.
+ *
+ * @param {string|!goog.dom.TagName<T>} tagName Tag to create.
+ * @param {?Object|?Array<string>|string=} opt_attributes If object, then a map
+ *     of name-value pairs for attributes. If a string, then this is the
+ *     className of the new element. If an array, the elements will be joined
+ *     together as the className of the new element.
+ * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
+ *     strings for text nodes. If one of the var_args is an array or NodeList,
+ *     its elements will be added as childNodes instead.
+ * @return {R} Reference to a DOM node. The return type is {!Element} if tagName
+ *     is a string or a more specific type if it is a member of
+ *     goog.dom.TagName (e.g. {!HTMLAnchorElement} for goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ */
+goog.dom.createDom = function(tagName, opt_attributes, var_args) {
+  return goog.dom.createDom_(document, arguments);
+};
+
+
+/**
+ * Helper for {@code createDom}.
+ * @param {!Document} doc The document to create the DOM in.
+ * @param {!Arguments} args Argument object passed from the callers. See
+ *     {@code goog.dom.createDom} for details.
+ * @return {!Element} Reference to a DOM node.
+ * @private
+ */
+goog.dom.createDom_ = function(doc, args) {
+  var tagName = String(args[0]);
+  var attributes = args[1];
+
+  // Internet Explorer is dumb:
+  // name: https://msdn.microsoft.com/en-us/library/ms534184(v=vs.85).aspx
+  // type: https://msdn.microsoft.com/en-us/library/ms534700(v=vs.85).aspx
+  // Also does not allow setting of 'type' attribute on 'input' or 'button'.
+  if (!goog.dom.BrowserFeature.CAN_ADD_NAME_OR_TYPE_ATTRIBUTES && attributes &&
+      (attributes.name || attributes.type)) {
+    var tagNameArr = ['<', tagName];
+    if (attributes.name) {
+      tagNameArr.push(' name="', goog.string.htmlEscape(attributes.name), '"');
+    }
+    if (attributes.type) {
+      tagNameArr.push(' type="', goog.string.htmlEscape(attributes.type), '"');
+
+      // Clone attributes map to remove 'type' without mutating the input.
+      var clone = {};
+      goog.object.extend(clone, attributes);
+
+      // JSCompiler can't see how goog.object.extend added this property,
+      // because it was essentially added by reflection.
+      // So it needs to be quoted.
+      delete clone['type'];
+
+      attributes = clone;
+    }
+    tagNameArr.push('>');
+    tagName = tagNameArr.join('');
+  }
+
+  var element = doc.createElement(tagName);
+
+  if (attributes) {
+    if (goog.isString(attributes)) {
+      element.className = attributes;
+    } else if (goog.isArray(attributes)) {
+      element.className = attributes.join(' ');
+    } else {
+      goog.dom.setProperties(element, attributes);
+    }
+  }
+
+  if (args.length > 2) {
+    goog.dom.append_(doc, element, args, 2);
+  }
+
+  return element;
+};
+
+
+/**
+ * Appends a node with text or other nodes.
+ * @param {!Document} doc The document to create new nodes in.
+ * @param {!Node} parent The node to append nodes to.
+ * @param {!Arguments} args The values to add. See {@code goog.dom.append}.
+ * @param {number} startIndex The index of the array to start from.
+ * @private
+ */
+goog.dom.append_ = function(doc, parent, args, startIndex) {
+  function childHandler(child) {
+    // TODO(pupius): More coercion, ala MochiKit?
+    if (child) {
+      parent.appendChild(
+          goog.isString(child) ? doc.createTextNode(child) : child);
+    }
+  }
+
+  for (var i = startIndex; i < args.length; i++) {
+    var arg = args[i];
+    // TODO(attila): Fix isArrayLike to return false for a text node.
+    if (goog.isArrayLike(arg) && !goog.dom.isNodeLike(arg)) {
+      // If the argument is a node list, not a real array, use a clone,
+      // because forEach can't be used to mutate a NodeList.
+      goog.array.forEach(
+          goog.dom.isNodeList(arg) ? goog.array.toArray(arg) : arg,
+          childHandler);
+    } else {
+      childHandler(arg);
+    }
+  }
+};
+
+
+/**
+ * Alias for {@code createDom}.
+ * @param {string|!goog.dom.TagName<T>} tagName Tag to create.
+ * @param {?Object|?Array<string>|string=} opt_attributes If object, then a map
+ *     of name-value pairs for attributes. If a string, then this is the
+ *     className of the new element. If an array, the elements will be joined
+ *     together as the className of the new element.
+ * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
+ *     strings for text nodes. If one of the var_args is an array, its
+ *     children will be added as childNodes instead.
+ * @return {R} Reference to a DOM node. The return type is {!Element} if tagName
+ *     is a string or a more specific type if it is a member of
+ *     goog.dom.TagName (e.g. {!HTMLAnchorElement} for goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ * @deprecated Use {@link goog.dom.createDom} instead.
+ */
+goog.dom.$dom = goog.dom.createDom;
+
+
+/**
+ * Creates a new element.
+ * @param {string|!goog.dom.TagName<T>} name Tag to create.
+ * @return {R} The new element. The return type is {!Element} if name is
+ *     a string or a more specific type if it is a member of goog.dom.TagName
+ *     (e.g. {!HTMLAnchorElement} for goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ */
+goog.dom.createElement = function(name) {
+  return goog.dom.createElement_(document, name);
+};
+
+
+/**
+ * Creates a new element.
+ * @param {!Document} doc The document to create the element in.
+ * @param {string|!goog.dom.TagName<T>} name Tag to create.
+ * @return {R} The new element. The return type is {!Element} if name is
+ *     a string or a more specific type if it is a member of goog.dom.TagName
+ *     (e.g. {!HTMLAnchorElement} for goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ * @private
+ */
+goog.dom.createElement_ = function(doc, name) {
+  return doc.createElement(String(name));
+};
+
+
+/**
+ * Creates a new text node.
+ * @param {number|string} content Content.
+ * @return {!Text} The new text node.
+ */
+goog.dom.createTextNode = function(content) {
+  return document.createTextNode(String(content));
+};
+
+
+/**
+ * Create a table.
+ * @param {number} rows The number of rows in the table.  Must be >= 1.
+ * @param {number} columns The number of columns in the table.  Must be >= 1.
+ * @param {boolean=} opt_fillWithNbsp If true, fills table entries with
+ *     {@code goog.string.Unicode.NBSP} characters.
+ * @return {!Element} The created table.
+ */
+goog.dom.createTable = function(rows, columns, opt_fillWithNbsp) {
+  // TODO(mlourenco): Return HTMLTableElement, also in prototype function.
+  // Callers need to be updated to e.g. not assign numbers to table.cellSpacing.
+  return goog.dom.createTable_(document, rows, columns, !!opt_fillWithNbsp);
+};
+
+
+/**
+ * Create a table.
+ * @param {!Document} doc Document object to use to create the table.
+ * @param {number} rows The number of rows in the table.  Must be >= 1.
+ * @param {number} columns The number of columns in the table.  Must be >= 1.
+ * @param {boolean} fillWithNbsp If true, fills table entries with
+ *     {@code goog.string.Unicode.NBSP} characters.
+ * @return {!HTMLTableElement} The created table.
+ * @private
+ */
+goog.dom.createTable_ = function(doc, rows, columns, fillWithNbsp) {
+  var table = goog.dom.createElement_(doc, goog.dom.TagName.TABLE);
+  var tbody =
+      table.appendChild(goog.dom.createElement_(doc, goog.dom.TagName.TBODY));
+  for (var i = 0; i < rows; i++) {
+    var tr = goog.dom.createElement_(doc, goog.dom.TagName.TR);
+    for (var j = 0; j < columns; j++) {
+      var td = goog.dom.createElement_(doc, goog.dom.TagName.TD);
+      // IE <= 9 will create a text node if we set text content to the empty
+      // string, so we avoid doing it unless necessary. This ensures that the
+      // same DOM tree is returned on all browsers.
+      if (fillWithNbsp) {
+        goog.dom.setTextContent(td, goog.string.Unicode.NBSP);
+      }
+      tr.appendChild(td);
+    }
+    tbody.appendChild(tr);
+  }
+  return table;
+};
+
+
+
+/**
+ * Creates a new Node from constant strings of HTML markup.
+ * @param {...!goog.string.Const} var_args The HTML strings to concatenate then
+ *     convert into a node.
+ * @return {!Node}
+ */
+goog.dom.constHtmlToNode = function(var_args) {
+  var stringArray = goog.array.map(arguments, goog.string.Const.unwrap);
+  var safeHtml =
+      goog.html.uncheckedconversions
+          .safeHtmlFromStringKnownToSatisfyTypeContract(
+              goog.string.Const.from(
+                  'Constant HTML string, that gets turned into a ' +
+                  'Node later, so it will be automatically balanced.'),
+              stringArray.join(''));
+  return goog.dom.safeHtmlToNode(safeHtml);
+};
+
+
+/**
+ * Converts HTML markup into a node. This is a safe version of
+ * {@code goog.dom.htmlToDocumentFragment} which is now deleted.
+ * @param {!goog.html.SafeHtml} html The HTML markup to convert.
+ * @return {!Node} The resulting node.
+ */
+goog.dom.safeHtmlToNode = function(html) {
+  return goog.dom.safeHtmlToNode_(document, html);
+};
+
+
+/**
+ * Helper for {@code safeHtmlToNode}.
+ * @param {!Document} doc The document.
+ * @param {!goog.html.SafeHtml} html The HTML markup to convert.
+ * @return {!Node} The resulting node.
+ * @private
+ */
+goog.dom.safeHtmlToNode_ = function(doc, html) {
+  var tempDiv = goog.dom.createElement_(doc, goog.dom.TagName.DIV);
+  if (goog.dom.BrowserFeature.INNER_HTML_NEEDS_SCOPED_ELEMENT) {
+    goog.dom.safe.setInnerHtml(
+        tempDiv, goog.html.SafeHtml.concat(goog.html.SafeHtml.BR, html));
+    tempDiv.removeChild(tempDiv.firstChild);
+  } else {
+    goog.dom.safe.setInnerHtml(tempDiv, html);
+  }
+  return goog.dom.childrenToNode_(doc, tempDiv);
+};
+
+
+/**
+ * Helper for {@code safeHtmlToNode_}.
+ * @param {!Document} doc The document.
+ * @param {!Node} tempDiv The input node.
+ * @return {!Node} The resulting node.
+ * @private
+ */
+goog.dom.childrenToNode_ = function(doc, tempDiv) {
+  if (tempDiv.childNodes.length == 1) {
+    return tempDiv.removeChild(tempDiv.firstChild);
+  } else {
+    var fragment = doc.createDocumentFragment();
+    while (tempDiv.firstChild) {
+      fragment.appendChild(tempDiv.firstChild);
+    }
+    return fragment;
+  }
+};
+
+
+/**
+ * Returns true if the browser is in "CSS1-compatible" (standards-compliant)
+ * mode, false otherwise.
+ * @return {boolean} True if in CSS1-compatible mode.
+ */
+goog.dom.isCss1CompatMode = function() {
+  return goog.dom.isCss1CompatMode_(document);
+};
+
+
+/**
+ * Returns true if the browser is in "CSS1-compatible" (standards-compliant)
+ * mode, false otherwise.
+ * @param {!Document} doc The document to check.
+ * @return {boolean} True if in CSS1-compatible mode.
+ * @private
+ */
+goog.dom.isCss1CompatMode_ = function(doc) {
+  if (goog.dom.COMPAT_MODE_KNOWN_) {
+    return goog.dom.ASSUME_STANDARDS_MODE;
+  }
+
+  return doc.compatMode == 'CSS1Compat';
+};
+
+
+/**
+ * Determines if the given node can contain children, intended to be used for
+ * HTML generation.
+ *
+ * IE natively supports node.canHaveChildren but has inconsistent behavior.
+ * Prior to IE8 the base tag allows children and in IE9 all nodes return true
+ * for canHaveChildren.
+ *
+ * In practice all non-IE browsers allow you to add children to any node, but
+ * the behavior is inconsistent:
+ *
+ * <pre>
+ *   var a = goog.dom.createElement(goog.dom.TagName.BR);
+ *   a.appendChild(document.createTextNode('foo'));
+ *   a.appendChild(document.createTextNode('bar'));
+ *   console.log(a.childNodes.length);  // 2
+ *   console.log(a.innerHTML);  // Chrome: "", IE9: "foobar", FF3.5: "foobar"
+ * </pre>
+ *
+ * For more information, see:
+ * http://dev.w3.org/html5/markup/syntax.html#syntax-elements
+ *
+ * TODO(pupius): Rename shouldAllowChildren() ?
+ *
+ * @param {Node} node The node to check.
+ * @return {boolean} Whether the node can contain children.
+ */
+goog.dom.canHaveChildren = function(node) {
+  if (node.nodeType != goog.dom.NodeType.ELEMENT) {
+    return false;
+  }
+  switch (/** @type {!Element} */ (node).tagName) {
+    case String(goog.dom.TagName.APPLET):
+    case String(goog.dom.TagName.AREA):
+    case String(goog.dom.TagName.BASE):
+    case String(goog.dom.TagName.BR):
+    case String(goog.dom.TagName.COL):
+    case String(goog.dom.TagName.COMMAND):
+    case String(goog.dom.TagName.EMBED):
+    case String(goog.dom.TagName.FRAME):
+    case String(goog.dom.TagName.HR):
+    case String(goog.dom.TagName.IMG):
+    case String(goog.dom.TagName.INPUT):
+    case String(goog.dom.TagName.IFRAME):
+    case String(goog.dom.TagName.ISINDEX):
+    case String(goog.dom.TagName.KEYGEN):
+    case String(goog.dom.TagName.LINK):
+    case String(goog.dom.TagName.NOFRAMES):
+    case String(goog.dom.TagName.NOSCRIPT):
+    case String(goog.dom.TagName.META):
+    case String(goog.dom.TagName.OBJECT):
+    case String(goog.dom.TagName.PARAM):
+    case String(goog.dom.TagName.SCRIPT):
+    case String(goog.dom.TagName.SOURCE):
+    case String(goog.dom.TagName.STYLE):
+    case String(goog.dom.TagName.TRACK):
+    case String(goog.dom.TagName.WBR):
+      return false;
+  }
+  return true;
+};
+
+
+/**
+ * Appends a child to a node.
+ * @param {Node} parent Parent.
+ * @param {Node} child Child.
+ */
+goog.dom.appendChild = function(parent, child) {
+  parent.appendChild(child);
+};
+
+
+/**
+ * Appends a node with text or other nodes.
+ * @param {!Node} parent The node to append nodes to.
+ * @param {...goog.dom.Appendable} var_args The things to append to the node.
+ *     If this is a Node it is appended as is.
+ *     If this is a string then a text node is appended.
+ *     If this is an array like object then fields 0 to length - 1 are appended.
+ */
+goog.dom.append = function(parent, var_args) {
+  goog.dom.append_(goog.dom.getOwnerDocument(parent), parent, arguments, 1);
+};
+
+
+/**
+ * Removes all the child nodes on a DOM node.
+ * @param {Node} node Node to remove children from.
+ */
+goog.dom.removeChildren = function(node) {
+  // Note: Iterations over live collections can be slow, this is the fastest
+  // we could find. The double parenthesis are used to prevent JsCompiler and
+  // strict warnings.
+  var child;
+  while ((child = node.firstChild)) {
+    node.removeChild(child);
+  }
+};
+
+
+/**
+ * Inserts a new node before an existing reference node (i.e. as the previous
+ * sibling). If the reference node has no parent, then does nothing.
+ * @param {Node} newNode Node to insert.
+ * @param {Node} refNode Reference node to insert before.
+ */
+goog.dom.insertSiblingBefore = function(newNode, refNode) {
+  if (refNode.parentNode) {
+    refNode.parentNode.insertBefore(newNode, refNode);
+  }
+};
+
+
+/**
+ * Inserts a new node after an existing reference node (i.e. as the next
+ * sibling). If the reference node has no parent, then does nothing.
+ * @param {Node} newNode Node to insert.
+ * @param {Node} refNode Reference node to insert after.
+ */
+goog.dom.insertSiblingAfter = function(newNode, refNode) {
+  if (refNode.parentNode) {
+    refNode.parentNode.insertBefore(newNode, refNode.nextSibling);
+  }
+};
+
+
+/**
+ * Insert a child at a given index. If index is larger than the number of child
+ * nodes that the parent currently has, the node is inserted as the last child
+ * node.
+ * @param {Element} parent The element into which to insert the child.
+ * @param {Node} child The element to insert.
+ * @param {number} index The index at which to insert the new child node. Must
+ *     not be negative.
+ */
+goog.dom.insertChildAt = function(parent, child, index) {
+  // Note that if the second argument is null, insertBefore
+  // will append the child at the end of the list of children.
+  parent.insertBefore(child, parent.childNodes[index] || null);
+};
+
+
+/**
+ * Removes a node from its parent.
+ * @param {Node} node The node to remove.
+ * @return {Node} The node removed if removed; else, null.
+ */
+goog.dom.removeNode = function(node) {
+  return node && node.parentNode ? node.parentNode.removeChild(node) : null;
+};
+
+
+/**
+ * Replaces a node in the DOM tree. Will do nothing if {@code oldNode} has no
+ * parent.
+ * @param {Node} newNode Node to insert.
+ * @param {Node} oldNode Node to replace.
+ */
+goog.dom.replaceNode = function(newNode, oldNode) {
+  var parent = oldNode.parentNode;
+  if (parent) {
+    parent.replaceChild(newNode, oldNode);
+  }
+};
+
+
+/**
+ * Flattens an element. That is, removes it and replace it with its children.
+ * Does nothing if the element is not in the document.
+ * @param {Element} element The element to flatten.
+ * @return {Element|undefined} The original element, detached from the document
+ *     tree, sans children; or undefined, if the element was not in the document
+ *     to begin with.
+ */
+goog.dom.flattenElement = function(element) {
+  var child, parent = element.parentNode;
+  if (parent && parent.nodeType != goog.dom.NodeType.DOCUMENT_FRAGMENT) {
+    // Use IE DOM method (supported by Opera too) if available
+    if (element.removeNode) {
+      return /** @type {Element} */ (element.removeNode(false));
+    } else {
+      // Move all children of the original node up one level.
+      while ((child = element.firstChild)) {
+        parent.insertBefore(child, element);
+      }
+
+      // Detach the original element.
+      return /** @type {Element} */ (goog.dom.removeNode(element));
+    }
+  }
+};
+
+
+/**
+ * Returns an array containing just the element children of the given element.
+ * @param {Element} element The element whose element children we want.
+ * @return {!(Array<!Element>|NodeList<!Element>)} An array or array-like list
+ *     of just the element children of the given element.
+ */
+goog.dom.getChildren = function(element) {
+  // We check if the children attribute is supported for child elements
+  // since IE8 misuses the attribute by also including comments.
+  if (goog.dom.BrowserFeature.CAN_USE_CHILDREN_ATTRIBUTE &&
+      element.children != undefined) {
+    return element.children;
+  }
+  // Fall back to manually filtering the element's child nodes.
+  return goog.array.filter(element.childNodes, function(node) {
+    return node.nodeType == goog.dom.NodeType.ELEMENT;
+  });
+};
+
+
+/**
+ * Returns the first child node that is an element.
+ * @param {Node} node The node to get the first child element of.
+ * @return {Element} The first child node of {@code node} that is an element.
+ */
+goog.dom.getFirstElementChild = function(node) {
+  if (goog.isDef(node.firstElementChild)) {
+    return /** @type {!Element} */ (node).firstElementChild;
+  }
+  return goog.dom.getNextElementNode_(node.firstChild, true);
+};
+
+
+/**
+ * Returns the last child node that is an element.
+ * @param {Node} node The node to get the last child element of.
+ * @return {Element} The last child node of {@code node} that is an element.
+ */
+goog.dom.getLastElementChild = function(node) {
+  if (goog.isDef(node.lastElementChild)) {
+    return /** @type {!Element} */ (node).lastElementChild;
+  }
+  return goog.dom.getNextElementNode_(node.lastChild, false);
+};
+
+
+/**
+ * Returns the first next sibling that is an element.
+ * @param {Node} node The node to get the next sibling element of.
+ * @return {Element} The next sibling of {@code node} that is an element.
+ */
+goog.dom.getNextElementSibling = function(node) {
+  if (goog.isDef(node.nextElementSibling)) {
+    return /** @type {!Element} */ (node).nextElementSibling;
+  }
+  return goog.dom.getNextElementNode_(node.nextSibling, true);
+};
+
+
+/**
+ * Returns the first previous sibling that is an element.
+ * @param {Node} node The node to get the previous sibling element of.
+ * @return {Element} The first previous sibling of {@code node} that is
+ *     an element.
+ */
+goog.dom.getPreviousElementSibling = function(node) {
+  if (goog.isDef(node.previousElementSibling)) {
+    return /** @type {!Element} */ (node).previousElementSibling;
+  }
+  return goog.dom.getNextElementNode_(node.previousSibling, false);
+};
+
+
+/**
+ * Returns the first node that is an element in the specified direction,
+ * starting with {@code node}.
+ * @param {Node} node The node to get the next element from.
+ * @param {boolean} forward Whether to look forwards or backwards.
+ * @return {Element} The first element.
+ * @private
+ */
+goog.dom.getNextElementNode_ = function(node, forward) {
+  while (node && node.nodeType != goog.dom.NodeType.ELEMENT) {
+    node = forward ? node.nextSibling : node.previousSibling;
+  }
+
+  return /** @type {Element} */ (node);
+};
+
+
+/**
+ * Returns the next node in source order from the given node.
+ * @param {Node} node The node.
+ * @return {Node} The next node in the DOM tree, or null if this was the last
+ *     node.
+ */
+goog.dom.getNextNode = function(node) {
+  if (!node) {
+    return null;
+  }
+
+  if (node.firstChild) {
+    return node.firstChild;
+  }
+
+  while (node && !node.nextSibling) {
+    node = node.parentNode;
+  }
+
+  return node ? node.nextSibling : null;
+};
+
+
+/**
+ * Returns the previous node in source order from the given node.
+ * @param {Node} node The node.
+ * @return {Node} The previous node in the DOM tree, or null if this was the
+ *     first node.
+ */
+goog.dom.getPreviousNode = function(node) {
+  if (!node) {
+    return null;
+  }
+
+  if (!node.previousSibling) {
+    return node.parentNode;
+  }
+
+  node = node.previousSibling;
+  while (node && node.lastChild) {
+    node = node.lastChild;
+  }
+
+  return node;
+};
+
+
+/**
+ * Whether the object looks like a DOM node.
+ * @param {?} obj The object being tested for node likeness.
+ * @return {boolean} Whether the object looks like a DOM node.
+ */
+goog.dom.isNodeLike = function(obj) {
+  return goog.isObject(obj) && obj.nodeType > 0;
+};
+
+
+/**
+ * Whether the object looks like an Element.
+ * @param {?} obj The object being tested for Element likeness.
+ * @return {boolean} Whether the object looks like an Element.
+ */
+goog.dom.isElement = function(obj) {
+  return goog.isObject(obj) && obj.nodeType == goog.dom.NodeType.ELEMENT;
+};
+
+
+/**
+ * Returns true if the specified value is a Window object. This includes the
+ * global window for HTML pages, and iframe windows.
+ * @param {?} obj Variable to test.
+ * @return {boolean} Whether the variable is a window.
+ */
+goog.dom.isWindow = function(obj) {
+  return goog.isObject(obj) && obj['window'] == obj;
+};
+
+
+/**
+ * Returns an element's parent, if it's an Element.
+ * @param {Element} element The DOM element.
+ * @return {Element} The parent, or null if not an Element.
+ */
+goog.dom.getParentElement = function(element) {
+  var parent;
+  if (goog.dom.BrowserFeature.CAN_USE_PARENT_ELEMENT_PROPERTY) {
+    var isIe9 = goog.userAgent.IE && goog.userAgent.isVersionOrHigher('9') &&
+        !goog.userAgent.isVersionOrHigher('10');
+    // SVG elements in IE9 can't use the parentElement property.
+    // goog.global['SVGElement'] is not defined in IE9 quirks mode.
+    if (!(isIe9 && goog.global['SVGElement'] &&
+          element instanceof goog.global['SVGElement'])) {
+      parent = element.parentElement;
+      if (parent) {
+        return parent;
+      }
+    }
+  }
+  parent = element.parentNode;
+  return goog.dom.isElement(parent) ? /** @type {!Element} */ (parent) : null;
+};
+
+
+/**
+ * Whether a node contains another node.
+ * @param {?Node|undefined} parent The node that should contain the other node.
+ * @param {?Node|undefined} descendant The node to test presence of.
+ * @return {boolean} Whether the parent node contains the descendent node.
+ */
+goog.dom.contains = function(parent, descendant) {
+  if (!parent || !descendant) {
+    return false;
+  }
+  // We use browser specific methods for this if available since it is faster
+  // that way.
+
+  // IE DOM
+  if (parent.contains && descendant.nodeType == goog.dom.NodeType.ELEMENT) {
+    return parent == descendant || parent.contains(descendant);
+  }
+
+  // W3C DOM Level 3
+  if (typeof parent.compareDocumentPosition != 'undefined') {
+    return parent == descendant ||
+        Boolean(parent.compareDocumentPosition(descendant) & 16);
+  }
+
+  // W3C DOM Level 1
+  while (descendant && parent != descendant) {
+    descendant = descendant.parentNode;
+  }
+  return descendant == parent;
+};
+
+
+/**
+ * Compares the document order of two nodes, returning 0 if they are the same
+ * node, a negative number if node1 is before node2, and a positive number if
+ * node2 is before node1.  Note that we compare the order the tags appear in the
+ * document so in the tree <b><i>text</i></b> the B node is considered to be
+ * before the I node.
+ *
+ * @param {Node} node1 The first node to compare.
+ * @param {Node} node2 The second node to compare.
+ * @return {number} 0 if the nodes are the same node, a negative number if node1
+ *     is before node2, and a positive number if node2 is before node1.
+ */
+goog.dom.compareNodeOrder = function(node1, node2) {
+  // Fall out quickly for equality.
+  if (node1 == node2) {
+    return 0;
+  }
+
+  // Use compareDocumentPosition where available
+  if (node1.compareDocumentPosition) {
+    // 4 is the bitmask for FOLLOWS.
+    return node1.compareDocumentPosition(node2) & 2 ? 1 : -1;
+  }
+
+  // Special case for document nodes on IE 7 and 8.
+  if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
+    if (node1.nodeType == goog.dom.NodeType.DOCUMENT) {
+      return -1;
+    }
+    if (node2.nodeType == goog.dom.NodeType.DOCUMENT) {
+      return 1;
+    }
+  }
+
+  // Process in IE using sourceIndex - we check to see if the first node has
+  // a source index or if its parent has one.
+  if ('sourceIndex' in node1 ||
+      (node1.parentNode && 'sourceIndex' in node1.parentNode)) {
+    var isElement1 = node1.nodeType == goog.dom.NodeType.ELEMENT;
+    var isElement2 = node2.nodeType == goog.dom.NodeType.ELEMENT;
+
+    if (isElement1 && isElement2) {
+      return node1.sourceIndex - node2.sourceIndex;
+    } else {
+      var parent1 = node1.parentNode;
+      var parent2 = node2.parentNode;
+
+      if (parent1 == parent2) {
+        return goog.dom.compareSiblingOrder_(node1, node2);
+      }
+
+      if (!isElement1 && goog.dom.contains(parent1, node2)) {
+        return -1 * goog.dom.compareParentsDescendantNodeIe_(node1, node2);
+      }
+
+
+      if (!isElement2 && goog.dom.contains(parent2, node1)) {
+        return goog.dom.compareParentsDescendantNodeIe_(node2, node1);
+      }
+
+      return (isElement1 ? node1.sourceIndex : parent1.sourceIndex) -
+          (isElement2 ? node2.sourceIndex : parent2.sourceIndex);
+    }
+  }
+
+  // For Safari, we compare ranges.
+  var doc = goog.dom.getOwnerDocument(node1);
+
+  var range1, range2;
+  range1 = doc.createRange();
+  range1.selectNode(node1);
+  range1.collapse(true);
+
+  range2 = doc.createRange();
+  range2.selectNode(node2);
+  range2.collapse(true);
+
+  return range1.compareBoundaryPoints(
+      goog.global['Range'].START_TO_END, range2);
+};
+
+
+/**
+ * Utility function to compare the position of two nodes, when
+ * {@code textNode}'s parent is an ancestor of {@code node}.  If this entry
+ * condition is not met, this function will attempt to reference a null object.
+ * @param {!Node} textNode The textNode to compare.
+ * @param {Node} node The node to compare.
+ * @return {number} -1 if node is before textNode, +1 otherwise.
+ * @private
+ */
+goog.dom.compareParentsDescendantNodeIe_ = function(textNode, node) {
+  var parent = textNode.parentNode;
+  if (parent == node) {
+    // If textNode is a child of node, then node comes first.
+    return -1;
+  }
+  var sibling = node;
+  while (sibling.parentNode != parent) {
+    sibling = sibling.parentNode;
+  }
+  return goog.dom.compareSiblingOrder_(sibling, textNode);
+};
+
+
+/**
+ * Utility function to compare the position of two nodes known to be non-equal
+ * siblings.
+ * @param {Node} node1 The first node to compare.
+ * @param {!Node} node2 The second node to compare.
+ * @return {number} -1 if node1 is before node2, +1 otherwise.
+ * @private
+ */
+goog.dom.compareSiblingOrder_ = function(node1, node2) {
+  var s = node2;
+  while ((s = s.previousSibling)) {
+    if (s == node1) {
+      // We just found node1 before node2.
+      return -1;
+    }
+  }
+
+  // Since we didn't find it, node1 must be after node2.
+  return 1;
+};
+
+
+/**
+ * Find the deepest common ancestor of the given nodes.
+ * @param {...Node} var_args The nodes to find a common ancestor of.
+ * @return {Node} The common ancestor of the nodes, or null if there is none.
+ *     null will only be returned if two or more of the nodes are from different
+ *     documents.
+ */
+goog.dom.findCommonAncestor = function(var_args) {
+  var i, count = arguments.length;
+  if (!count) {
+    return null;
+  } else if (count == 1) {
+    return arguments[0];
+  }
+
+  var paths = [];
+  var minLength = Infinity;
+  for (i = 0; i < count; i++) {
+    // Compute the list of ancestors.
+    var ancestors = [];
+    var node = arguments[i];
+    while (node) {
+      ancestors.unshift(node);
+      node = node.parentNode;
+    }
+
+    // Save the list for comparison.
+    paths.push(ancestors);
+    minLength = Math.min(minLength, ancestors.length);
+  }
+  var output = null;
+  for (i = 0; i < minLength; i++) {
+    var first = paths[0][i];
+    for (var j = 1; j < count; j++) {
+      if (first != paths[j][i]) {
+        return output;
+      }
+    }
+    output = first;
+  }
+  return output;
+};
+
+
+/**
+ * Returns the owner document for a node.
+ * @param {Node|Window} node The node to get the document for.
+ * @return {!Document} The document owning the node.
+ */
+goog.dom.getOwnerDocument = function(node) {
+  // TODO(nnaze): Update param signature to be non-nullable.
+  goog.asserts.assert(node, 'Node cannot be null or undefined.');
+  return /** @type {!Document} */ (
+      node.nodeType == goog.dom.NodeType.DOCUMENT ? node : node.ownerDocument ||
+              node.document);
+};
+
+
+/**
+ * Cross-browser function for getting the document element of a frame or iframe.
+ * @param {Element} frame Frame element.
+ * @return {!Document} The frame content document.
+ */
+goog.dom.getFrameContentDocument = function(frame) {
+  return frame.contentDocument ||
+      /** @type {!HTMLFrameElement} */ (frame).contentWindow.document;
+};
+
+
+/**
+ * Cross-browser function for getting the window of a frame or iframe.
+ * @param {Element} frame Frame element.
+ * @return {Window} The window associated with the given frame, or null if none
+ *     exists.
+ */
+goog.dom.getFrameContentWindow = function(frame) {
+  try {
+    return frame.contentWindow ||
+        (frame.contentDocument ? goog.dom.getWindow(frame.contentDocument) :
+                                 null);
+  } catch (e) {
+    // NOTE(jfedor): In IE8, checking the contentWindow or contentDocument
+    // properties will throw a "Unspecified Error" exception if the iframe is
+    // not inserted in the DOM. If we get this we can be sure that no window
+    // exists, so return null.
+  }
+  return null;
+};
+
+
+/**
+ * Sets the text content of a node, with cross-browser support.
+ * @param {Node} node The node to change the text content of.
+ * @param {string|number} text The value that should replace the node's content.
+ */
+goog.dom.setTextContent = function(node, text) {
+  goog.asserts.assert(
+      node != null,
+      'goog.dom.setTextContent expects a non-null value for node');
+
+  if ('textContent' in node) {
+    node.textContent = text;
+  } else if (node.nodeType == goog.dom.NodeType.TEXT) {
+    /** @type {!Text} */ (node).data = String(text);
+  } else if (
+      node.firstChild && node.firstChild.nodeType == goog.dom.NodeType.TEXT) {
+    // If the first child is a text node we just change its data and remove the
+    // rest of the children.
+    while (node.lastChild != node.firstChild) {
+      node.removeChild(node.lastChild);
+    }
+    /** @type {!Text} */ (node.firstChild).data = String(text);
+  } else {
+    goog.dom.removeChildren(node);
+    var doc = goog.dom.getOwnerDocument(node);
+    node.appendChild(doc.createTextNode(String(text)));
+  }
+};
+
+
+/**
+ * Gets the outerHTML of a node, which islike innerHTML, except that it
+ * actually contains the HTML of the node itself.
+ * @param {Element} element The element to get the HTML of.
+ * @return {string} The outerHTML of the given element.
+ */
+goog.dom.getOuterHtml = function(element) {
+  goog.asserts.assert(
+      element !== null,
+      'goog.dom.getOuterHtml expects a non-null value for element');
+  // IE, Opera and WebKit all have outerHTML.
+  if ('outerHTML' in element) {
+    return element.outerHTML;
+  } else {
+    var doc = goog.dom.getOwnerDocument(element);
+    var div = goog.dom.createElement_(doc, goog.dom.TagName.DIV);
+    div.appendChild(element.cloneNode(true));
+    return div.innerHTML;
+  }
+};
+
+
+/**
+ * Finds the first descendant node that matches the filter function, using
+ * a depth first search. This function offers the most general purpose way
+ * of finding a matching element. You may also wish to consider
+ * {@code goog.dom.query} which can express many matching criteria using
+ * CSS selector expressions. These expressions often result in a more
+ * compact representation of the desired result.
+ * @see goog.dom.query
+ *
+ * @param {Node} root The root of the tree to search.
+ * @param {function(Node) : boolean} p The filter function.
+ * @return {Node|undefined} The found node or undefined if none is found.
+ */
+goog.dom.findNode = function(root, p) {
+  var rv = [];
+  var found = goog.dom.findNodes_(root, p, rv, true);
+  return found ? rv[0] : undefined;
+};
+
+
+/**
+ * Finds all the descendant nodes that match the filter function, using a
+ * a depth first search. This function offers the most general-purpose way
+ * of finding a set of matching elements. You may also wish to consider
+ * {@code goog.dom.query} which can express many matching criteria using
+ * CSS selector expressions. These expressions often result in a more
+ * compact representation of the desired result.
+
+ * @param {Node} root The root of the tree to search.
+ * @param {function(Node) : boolean} p The filter function.
+ * @return {!Array<!Node>} The found nodes or an empty array if none are found.
+ */
+goog.dom.findNodes = function(root, p) {
+  var rv = [];
+  goog.dom.findNodes_(root, p, rv, false);
+  return rv;
+};
+
+
+/**
+ * Finds the first or all the descendant nodes that match the filter function,
+ * using a depth first search.
+ * @param {Node} root The root of the tree to search.
+ * @param {function(Node) : boolean} p The filter function.
+ * @param {!Array<!Node>} rv The found nodes are added to this array.
+ * @param {boolean} findOne If true we exit after the first found node.
+ * @return {boolean} Whether the search is complete or not. True in case findOne
+ *     is true and the node is found. False otherwise.
+ * @private
+ */
+goog.dom.findNodes_ = function(root, p, rv, findOne) {
+  if (root != null) {
+    var child = root.firstChild;
+    while (child) {
+      if (p(child)) {
+        rv.push(child);
+        if (findOne) {
+          return true;
+        }
+      }
+      if (goog.dom.findNodes_(child, p, rv, findOne)) {
+        return true;
+      }
+      child = child.nextSibling;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Map of tags whose content to ignore when calculating text length.
+ * @private {!Object<string, number>}
+ * @const
+ */
+goog.dom.TAGS_TO_IGNORE_ = {
+  'SCRIPT': 1,
+  'STYLE': 1,
+  'HEAD': 1,
+  'IFRAME': 1,
+  'OBJECT': 1
+};
+
+
+/**
+ * Map of tags which have predefined values with regard to whitespace.
+ * @private {!Object<string, string>}
+ * @const
+ */
+goog.dom.PREDEFINED_TAG_VALUES_ = {
+  'IMG': ' ',
+  'BR': '\n'
+};
+
+
+/**
+ * Returns true if the element has a tab index that allows it to receive
+ * keyboard focus (tabIndex >= 0), false otherwise.  Note that some elements
+ * natively support keyboard focus, even if they have no tab index.
+ * @param {!Element} element Element to check.
+ * @return {boolean} Whether the element has a tab index that allows keyboard
+ *     focus.
+ */
+goog.dom.isFocusableTabIndex = function(element) {
+  return goog.dom.hasSpecifiedTabIndex_(element) &&
+      goog.dom.isTabIndexFocusable_(element);
+};
+
+
+/**
+ * Enables or disables keyboard focus support on the element via its tab index.
+ * Only elements for which {@link goog.dom.isFocusableTabIndex} returns true
+ * (or elements that natively support keyboard focus, like form elements) can
+ * receive keyboard focus.  See http://go/tabindex for more info.
+ * @param {Element} element Element whose tab index is to be changed.
+ * @param {boolean} enable Whether to set or remove a tab index on the element
+ *     that supports keyboard focus.
+ */
+goog.dom.setFocusableTabIndex = function(element, enable) {
+  if (enable) {
+    element.tabIndex = 0;
+  } else {
+    // Set tabIndex to -1 first, then remove it. This is a workaround for
+    // Safari (confirmed in version 4 on Windows). When removing the attribute
+    // without setting it to -1 first, the element remains keyboard focusable
+    // despite not having a tabIndex attribute anymore.
+    element.tabIndex = -1;
+    element.removeAttribute('tabIndex');  // Must be camelCase!
+  }
+};
+
+
+/**
+ * Returns true if the element can be focused, i.e. it has a tab index that
+ * allows it to receive keyboard focus (tabIndex >= 0), or it is an element
+ * that natively supports keyboard focus.
+ * @param {!Element} element Element to check.
+ * @return {boolean} Whether the element allows keyboard focus.
+ */
+goog.dom.isFocusable = function(element) {
+  var focusable;
+  // Some elements can have unspecified tab index and still receive focus.
+  if (goog.dom.nativelySupportsFocus_(element)) {
+    // Make sure the element is not disabled ...
+    focusable = !element.disabled &&
+        // ... and if a tab index is specified, it allows focus.
+        (!goog.dom.hasSpecifiedTabIndex_(element) ||
+         goog.dom.isTabIndexFocusable_(element));
+  } else {
+    focusable = goog.dom.isFocusableTabIndex(element);
+  }
+
+  // IE requires elements to be visible in order to focus them.
+  return focusable && goog.userAgent.IE ?
+      goog.dom.hasNonZeroBoundingRect_(/** @type {!HTMLElement} */ (element)) :
+      focusable;
+};
+
+
+/**
+ * Returns true if the element has a specified tab index.
+ * @param {!Element} element Element to check.
+ * @return {boolean} Whether the element has a specified tab index.
+ * @private
+ */
+goog.dom.hasSpecifiedTabIndex_ = function(element) {
+  // IE8 and below don't support hasAttribute(), instead check whether the
+  // 'tabindex' attributeNode is specified. Otherwise check hasAttribute().
+  if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9')) {
+    var attrNode = element.getAttributeNode('tabindex');  // Must be lowercase!
+    return goog.isDefAndNotNull(attrNode) && attrNode.specified;
+  } else {
+    return element.hasAttribute('tabindex');
+  }
+};
+
+
+/**
+ * Returns true if the element's tab index allows the element to be focused.
+ * @param {!Element} element Element to check.
+ * @return {boolean} Whether the element's tab index allows focus.
+ * @private
+ */
+goog.dom.isTabIndexFocusable_ = function(element) {
+  var index = /** @type {!HTMLElement} */ (element).tabIndex;
+  // NOTE: IE9 puts tabIndex in 16-bit int, e.g. -2 is 65534.
+  return goog.isNumber(index) && index >= 0 && index < 32768;
+};
+
+
+/**
+ * Returns true if the element is focusable even when tabIndex is not set.
+ * @param {!Element} element Element to check.
+ * @return {boolean} Whether the element natively supports focus.
+ * @private
+ */
+goog.dom.nativelySupportsFocus_ = function(element) {
+  return element.tagName == goog.dom.TagName.A ||
+      element.tagName == goog.dom.TagName.INPUT ||
+      element.tagName == goog.dom.TagName.TEXTAREA ||
+      element.tagName == goog.dom.TagName.SELECT ||
+      element.tagName == goog.dom.TagName.BUTTON;
+};
+
+
+/**
+ * Returns true if the element has a bounding rectangle that would be visible
+ * (i.e. its width and height are greater than zero).
+ * @param {!HTMLElement} element Element to check.
+ * @return {boolean} Whether the element has a non-zero bounding rectangle.
+ * @private
+ */
+goog.dom.hasNonZeroBoundingRect_ = function(element) {
+  var rect;
+  if (!goog.isFunction(element['getBoundingClientRect']) ||
+      // In IE, getBoundingClientRect throws on detached nodes.
+      (goog.userAgent.IE && element.parentElement == null)) {
+    rect = {'height': element.offsetHeight, 'width': element.offsetWidth};
+  } else {
+    rect = element.getBoundingClientRect();
+  }
+  return goog.isDefAndNotNull(rect) && rect.height > 0 && rect.width > 0;
+};
+
+
+/**
+ * Returns the text content of the current node, without markup and invisible
+ * symbols. New lines are stripped and whitespace is collapsed,
+ * such that each character would be visible.
+ *
+ * In browsers that support it, innerText is used.  Other browsers attempt to
+ * simulate it via node traversal.  Line breaks are canonicalized in IE.
+ *
+ * @param {Node} node The node from which we are getting content.
+ * @return {string} The text content.
+ */
+goog.dom.getTextContent = function(node) {
+  var textContent;
+  // Note(arv): IE9, Opera, and Safari 3 support innerText but they include
+  // text nodes in script tags. So we revert to use a user agent test here.
+  if (goog.dom.BrowserFeature.CAN_USE_INNER_TEXT && node !== null &&
+      ('innerText' in node)) {
+    textContent = goog.string.canonicalizeNewlines(node.innerText);
+    // Unfortunately .innerText() returns text with &shy; symbols
+    // We need to filter it out and then remove duplicate whitespaces
+  } else {
+    var buf = [];
+    goog.dom.getTextContent_(node, buf, true);
+    textContent = buf.join('');
+  }
+
+  // Strip &shy; entities. goog.format.insertWordBreaks inserts them in Opera.
+  textContent = textContent.replace(/ \xAD /g, ' ').replace(/\xAD/g, '');
+  // Strip &#8203; entities. goog.format.insertWordBreaks inserts them in IE8.
+  textContent = textContent.replace(/\u200B/g, '');
+
+  // Skip this replacement on old browsers with working innerText, which
+  // automatically turns &nbsp; into ' ' and / +/ into ' ' when reading
+  // innerText.
+  if (!goog.dom.BrowserFeature.CAN_USE_INNER_TEXT) {
+    textContent = textContent.replace(/ +/g, ' ');
+  }
+  if (textContent != ' ') {
+    textContent = textContent.replace(/^\s*/, '');
+  }
+
+  return textContent;
+};
+
+
+/**
+ * Returns the text content of the current node, without markup.
+ *
+ * Unlike {@code getTextContent} this method does not collapse whitespaces
+ * or normalize lines breaks.
+ *
+ * @param {Node} node The node from which we are getting content.
+ * @return {string} The raw text content.
+ */
+goog.dom.getRawTextContent = function(node) {
+  var buf = [];
+  goog.dom.getTextContent_(node, buf, false);
+
+  return buf.join('');
+};
+
+
+/**
+ * Recursive support function for text content retrieval.
+ *
+ * @param {Node} node The node from which we are getting content.
+ * @param {Array<string>} buf string buffer.
+ * @param {boolean} normalizeWhitespace Whether to normalize whitespace.
+ * @private
+ */
+goog.dom.getTextContent_ = function(node, buf, normalizeWhitespace) {
+  if (node.nodeName in goog.dom.TAGS_TO_IGNORE_) {
+    // ignore certain tags
+  } else if (node.nodeType == goog.dom.NodeType.TEXT) {
+    if (normalizeWhitespace) {
+      buf.push(String(node.nodeValue).replace(/(\r\n|\r|\n)/g, ''));
+    } else {
+      buf.push(node.nodeValue);
+    }
+  } else if (node.nodeName in goog.dom.PREDEFINED_TAG_VALUES_) {
+    buf.push(goog.dom.PREDEFINED_TAG_VALUES_[node.nodeName]);
+  } else {
+    var child = node.firstChild;
+    while (child) {
+      goog.dom.getTextContent_(child, buf, normalizeWhitespace);
+      child = child.nextSibling;
+    }
+  }
+};
+
+
+/**
+ * Returns the text length of the text contained in a node, without markup. This
+ * is equivalent to the selection length if the node was selected, or the number
+ * of cursor movements to traverse the node. Images & BRs take one space.  New
+ * lines are ignored.
+ *
+ * @param {Node} node The node whose text content length is being calculated.
+ * @return {number} The length of {@code node}'s text content.
+ */
+goog.dom.getNodeTextLength = function(node) {
+  return goog.dom.getTextContent(node).length;
+};
+
+
+/**
+ * Returns the text offset of a node relative to one of its ancestors. The text
+ * length is the same as the length calculated by goog.dom.getNodeTextLength.
+ *
+ * @param {Node} node The node whose offset is being calculated.
+ * @param {Node=} opt_offsetParent The node relative to which the offset will
+ *     be calculated. Defaults to the node's owner document's body.
+ * @return {number} The text offset.
+ */
+goog.dom.getNodeTextOffset = function(node, opt_offsetParent) {
+  var root = opt_offsetParent || goog.dom.getOwnerDocument(node).body;
+  var buf = [];
+  while (node && node != root) {
+    var cur = node;
+    while ((cur = cur.previousSibling)) {
+      buf.unshift(goog.dom.getTextContent(cur));
+    }
+    node = node.parentNode;
+  }
+  // Trim left to deal with FF cases when there might be line breaks and empty
+  // nodes at the front of the text
+  return goog.string.trimLeft(buf.join('')).replace(/ +/g, ' ').length;
+};
+
+
+/**
+ * Returns the node at a given offset in a parent node.  If an object is
+ * provided for the optional third parameter, the node and the remainder of the
+ * offset will stored as properties of this object.
+ * @param {Node} parent The parent node.
+ * @param {number} offset The offset into the parent node.
+ * @param {Object=} opt_result Object to be used to store the return value. The
+ *     return value will be stored in the form {node: Node, remainder: number}
+ *     if this object is provided.
+ * @return {Node} The node at the given offset.
+ */
+goog.dom.getNodeAtOffset = function(parent, offset, opt_result) {
+  var stack = [parent], pos = 0, cur = null;
+  while (stack.length > 0 && pos < offset) {
+    cur = stack.pop();
+    if (cur.nodeName in goog.dom.TAGS_TO_IGNORE_) {
+      // ignore certain tags
+    } else if (cur.nodeType == goog.dom.NodeType.TEXT) {
+      var text = cur.nodeValue.replace(/(\r\n|\r|\n)/g, '').replace(/ +/g, ' ');
+      pos += text.length;
+    } else if (cur.nodeName in goog.dom.PREDEFINED_TAG_VALUES_) {
+      pos += goog.dom.PREDEFINED_TAG_VALUES_[cur.nodeName].length;
+    } else {
+      for (var i = cur.childNodes.length - 1; i >= 0; i--) {
+        stack.push(cur.childNodes[i]);
+      }
+    }
+  }
+  if (goog.isObject(opt_result)) {
+    opt_result.remainder = cur ? cur.nodeValue.length + offset - pos - 1 : 0;
+    opt_result.node = cur;
+  }
+
+  return cur;
+};
+
+
+/**
+ * Returns true if the object is a {@code NodeList}.  To qualify as a NodeList,
+ * the object must have a numeric length property and an item function (which
+ * has type 'string' on IE for some reason).
+ * @param {Object} val Object to test.
+ * @return {boolean} Whether the object is a NodeList.
+ */
+goog.dom.isNodeList = function(val) {
+  // TODO(attila): Now the isNodeList is part of goog.dom we can use
+  // goog.userAgent to make this simpler.
+  // A NodeList must have a length property of type 'number' on all platforms.
+  if (val && typeof val.length == 'number') {
+    // A NodeList is an object everywhere except Safari, where it's a function.
+    if (goog.isObject(val)) {
+      // A NodeList must have an item function (on non-IE platforms) or an item
+      // property of type 'string' (on IE).
+      return typeof val.item == 'function' || typeof val.item == 'string';
+    } else if (goog.isFunction(val)) {
+      // On Safari, a NodeList is a function with an item property that is also
+      // a function.
+      return typeof val.item == 'function';
+    }
+  }
+
+  // Not a NodeList.
+  return false;
+};
+
+
+/**
+ * Walks up the DOM hierarchy returning the first ancestor that has the passed
+ * tag name and/or class name. If the passed element matches the specified
+ * criteria, the element itself is returned.
+ * @param {Node} element The DOM node to start with.
+ * @param {?(goog.dom.TagName<T>|string)=} opt_tag The tag name to match (or
+ *     null/undefined to match only based on class name).
+ * @param {?string=} opt_class The class name to match (or null/undefined to
+ *     match only based on tag name).
+ * @param {number=} opt_maxSearchSteps Maximum number of levels to search up the
+ *     dom.
+ * @return {?R} The first ancestor that matches the passed criteria, or
+ *     null if no match is found. The return type is {?Element} if opt_tag is
+ *     not a member of goog.dom.TagName or a more specific type if it is (e.g.
+ *     {?HTMLAnchorElement} for goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ */
+goog.dom.getAncestorByTagNameAndClass = function(
+    element, opt_tag, opt_class, opt_maxSearchSteps) {
+  if (!opt_tag && !opt_class) {
+    return null;
+  }
+  var tagName = opt_tag ? String(opt_tag).toUpperCase() : null;
+  return /** @type {Element} */ (goog.dom.getAncestor(element, function(node) {
+    return (!tagName || node.nodeName == tagName) &&
+        (!opt_class ||
+         goog.isString(node.className) &&
+             goog.array.contains(node.className.split(/\s+/), opt_class));
+  }, true, opt_maxSearchSteps));
+};
+
+
+/**
+ * Walks up the DOM hierarchy returning the first ancestor that has the passed
+ * class name. If the passed element matches the specified criteria, the
+ * element itself is returned.
+ * @param {Node} element The DOM node to start with.
+ * @param {string} className The class name to match.
+ * @param {number=} opt_maxSearchSteps Maximum number of levels to search up the
+ *     dom.
+ * @return {Element} The first ancestor that matches the passed criteria, or
+ *     null if none match.
+ */
+goog.dom.getAncestorByClass = function(element, className, opt_maxSearchSteps) {
+  return goog.dom.getAncestorByTagNameAndClass(
+      element, null, className, opt_maxSearchSteps);
+};
+
+
+/**
+ * Walks up the DOM hierarchy returning the first ancestor that passes the
+ * matcher function.
+ * @param {Node} element The DOM node to start with.
+ * @param {function(Node) : boolean} matcher A function that returns true if the
+ *     passed node matches the desired criteria.
+ * @param {boolean=} opt_includeNode If true, the node itself is included in
+ *     the search (the first call to the matcher will pass startElement as
+ *     the node to test).
+ * @param {number=} opt_maxSearchSteps Maximum number of levels to search up the
+ *     dom.
+ * @return {Node} DOM node that matched the matcher, or null if there was
+ *     no match.
+ */
+goog.dom.getAncestor = function(
+    element, matcher, opt_includeNode, opt_maxSearchSteps) {
+  if (element && !opt_includeNode) {
+    element = element.parentNode;
+  }
+  var steps = 0;
+  while (element &&
+         (opt_maxSearchSteps == null || steps <= opt_maxSearchSteps)) {
+    goog.asserts.assert(element.name != 'parentNode');
+    if (matcher(element)) {
+      return element;
+    }
+    element = element.parentNode;
+    steps++;
+  }
+  // Reached the root of the DOM without a match
+  return null;
+};
+
+
+/**
+ * Determines the active element in the given document.
+ * @param {Document} doc The document to look in.
+ * @return {Element} The active element.
+ */
+goog.dom.getActiveElement = function(doc) {
+  try {
+    return doc && doc.activeElement;
+  } catch (e) {
+    // NOTE(nicksantos): Sometimes, evaluating document.activeElement in IE
+    // throws an exception. I'm not 100% sure why, but I suspect it chokes
+    // on document.activeElement if the activeElement has been recently
+    // removed from the DOM by a JS operation.
+    //
+    // We assume that an exception here simply means
+    // "there is no active element."
+  }
+
+  return null;
+};
+
+
+/**
+ * Gives the current devicePixelRatio.
+ *
+ * By default, this is the value of window.devicePixelRatio (which should be
+ * preferred if present).
+ *
+ * If window.devicePixelRatio is not present, the ratio is calculated with
+ * window.matchMedia, if present. Otherwise, gives 1.0.
+ *
+ * Some browsers (including Chrome) consider the browser zoom level in the pixel
+ * ratio, so the value may change across multiple calls.
+ *
+ * @return {number} The number of actual pixels per virtual pixel.
+ */
+goog.dom.getPixelRatio = function() {
+  var win = goog.dom.getWindow();
+  if (goog.isDef(win.devicePixelRatio)) {
+    return win.devicePixelRatio;
+  } else if (win.matchMedia) {
+    // Should be for IE10 and FF6-17 (this basically clamps to lower)
+    // Note that the order of these statements is important
+    return goog.dom.matchesPixelRatio_(3) || goog.dom.matchesPixelRatio_(2) ||
+           goog.dom.matchesPixelRatio_(1.5) || goog.dom.matchesPixelRatio_(1) ||
+           .75;
+  }
+  return 1;
+};
+
+
+/**
+ * Calculates a mediaQuery to check if the current device supports the
+ * given actual to virtual pixel ratio.
+ * @param {number} pixelRatio The ratio of actual pixels to virtual pixels.
+ * @return {number} pixelRatio if applicable, otherwise 0.
+ * @private
+ */
+goog.dom.matchesPixelRatio_ = function(pixelRatio) {
+  var win = goog.dom.getWindow();
+  /**
+   * Due to the 1:96 fixed ratio of CSS in to CSS px, 1dppx is equivalent to
+   * 96dpi.
+   * @const {number}
+   */
+  var dpiPerDppx = 96;
+  var query =
+      // FF16-17
+      '(min-resolution: ' + pixelRatio + 'dppx),' +
+      // FF6-15
+      '(min--moz-device-pixel-ratio: ' + pixelRatio + '),' +
+      // IE10 (this works for the two browsers above too but I don't want to
+      // trust the 1:96 fixed ratio magic)
+      '(min-resolution: ' + (pixelRatio * dpiPerDppx) + 'dpi)';
+  return win.matchMedia(query).matches ? pixelRatio : 0;
+};
+
+
+/**
+ * Gets '2d' context of a canvas. Shortcut for canvas.getContext('2d') with a
+ * type information.
+ * @param {!HTMLCanvasElement} canvas
+ * @return {!CanvasRenderingContext2D}
+ */
+goog.dom.getCanvasContext2D = function(canvas) {
+  return /** @type {!CanvasRenderingContext2D} */ (canvas.getContext('2d'));
+};
+
+
+
+/**
+ * Create an instance of a DOM helper with a new document object.
+ * @param {Document=} opt_document Document object to associate with this
+ *     DOM helper.
+ * @constructor
+ */
+goog.dom.DomHelper = function(opt_document) {
+  /**
+   * Reference to the document object to use
+   * @type {!Document}
+   * @private
+   */
+  this.document_ = opt_document || goog.global.document || document;
+};
+
+
+/**
+ * Gets the dom helper object for the document where the element resides.
+ * @param {Node=} opt_node If present, gets the DomHelper for this node.
+ * @return {!goog.dom.DomHelper} The DomHelper.
+ */
+goog.dom.DomHelper.prototype.getDomHelper = goog.dom.getDomHelper;
+
+
+/**
+ * Sets the document object.
+ * @param {!Document} document Document object.
+ */
+goog.dom.DomHelper.prototype.setDocument = function(document) {
+  this.document_ = document;
+};
+
+
+/**
+ * Gets the document object being used by the dom library.
+ * @return {!Document} Document object.
+ */
+goog.dom.DomHelper.prototype.getDocument = function() {
+  return this.document_;
+};
+
+
+/**
+ * Alias for {@code getElementById}. If a DOM node is passed in then we just
+ * return that.
+ * @param {string|Element} element Element ID or a DOM node.
+ * @return {Element} The element with the given ID, or the node passed in.
+ */
+goog.dom.DomHelper.prototype.getElement = function(element) {
+  return goog.dom.getElementHelper_(this.document_, element);
+};
+
+
+/**
+ * Gets an element by id, asserting that the element is found.
+ *
+ * This is used when an element is expected to exist, and should fail with
+ * an assertion error if it does not (if assertions are enabled).
+ *
+ * @param {string} id Element ID.
+ * @return {!Element} The element with the given ID, if it exists.
+ */
+goog.dom.DomHelper.prototype.getRequiredElement = function(id) {
+  return goog.dom.getRequiredElementHelper_(this.document_, id);
+};
+
+
+/**
+ * Alias for {@code getElement}.
+ * @param {string|Element} element Element ID or a DOM node.
+ * @return {Element} The element with the given ID, or the node passed in.
+ * @deprecated Use {@link goog.dom.DomHelper.prototype.getElement} instead.
+ */
+goog.dom.DomHelper.prototype.$ = goog.dom.DomHelper.prototype.getElement;
+
+
+/**
+ * Gets elements by tag name.
+ * @param {!goog.dom.TagName<T>} tagName
+ * @param {(!Document|!Element)=} opt_parent Parent element or document where to
+ *     look for elements. Defaults to document of this DomHelper.
+ * @return {!NodeList<R>} List of elements. The members of the list are
+ *     {!Element} if tagName is not a member of goog.dom.TagName or more
+ *     specific types if it is (e.g. {!HTMLAnchorElement} for
+ *     goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ */
+goog.dom.DomHelper.prototype.getElementsByTagName =
+    function(tagName, opt_parent) {
+  var parent = opt_parent || this.document_;
+  return parent.getElementsByTagName(String(tagName));
+};
+
+
+/**
+ * Looks up elements by both tag and class name, using browser native functions
+ * ({@code querySelectorAll}, {@code getElementsByTagName} or
+ * {@code getElementsByClassName}) where possible. The returned array is a live
+ * NodeList or a static list depending on the code path taken.
+ *
+ * @see goog.dom.query
+ *
+ * @param {(string|?goog.dom.TagName<T>)=} opt_tag Element tag name or * for all
+ *     tags.
+ * @param {?string=} opt_class Optional class name.
+ * @param {(Document|Element)=} opt_el Optional element to look in.
+ * @return {!IArrayLike<R>} Array-like list of elements (only a length property
+ *     and numerical indices are guaranteed to exist). The members of the array
+ *     are {!Element} if opt_tag is not a member of goog.dom.TagName or more
+ *     specific types if it is (e.g. {!HTMLAnchorElement} for
+ *     goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ */
+goog.dom.DomHelper.prototype.getElementsByTagNameAndClass = function(
+    opt_tag, opt_class, opt_el) {
+  return goog.dom.getElementsByTagNameAndClass_(
+      this.document_, opt_tag, opt_class, opt_el);
+};
+
+
+/**
+ * Gets the first element matching the tag and the class.
+ *
+ * @param {(string|?goog.dom.TagName<T>)=} opt_tag Element tag name.
+ * @param {?string=} opt_class Optional class name.
+ * @param {(Document|Element)=} opt_el Optional element to look in.
+ * @return {?R} Reference to a DOM node. The return type is {?Element} if
+ *     tagName is a string or a more specific type if it is a member of
+ *     goog.dom.TagName (e.g. {?HTMLAnchorElement} for goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ */
+goog.dom.DomHelper.prototype.getElementByTagNameAndClass = function(
+    opt_tag, opt_class, opt_el) {
+  return goog.dom.getElementByTagNameAndClass_(
+      this.document_, opt_tag, opt_class, opt_el);
+};
+
+
+/**
+ * Returns an array of all the elements with the provided className.
+ * @see {goog.dom.query}
+ * @param {string} className the name of the class to look for.
+ * @param {Element|Document=} opt_el Optional element to look in.
+ * @return {!IArrayLike<!Element>} The items found with the class name provided.
+ */
+goog.dom.DomHelper.prototype.getElementsByClass = function(className, opt_el) {
+  var doc = opt_el || this.document_;
+  return goog.dom.getElementsByClass(className, doc);
+};
+
+
+/**
+ * Returns the first element we find matching the provided class name.
+ * @see {goog.dom.query}
+ * @param {string} className the name of the class to look for.
+ * @param {(Element|Document)=} opt_el Optional element to look in.
+ * @return {Element} The first item found with the class name provided.
+ */
+goog.dom.DomHelper.prototype.getElementByClass = function(className, opt_el) {
+  var doc = opt_el || this.document_;
+  return goog.dom.getElementByClass(className, doc);
+};
+
+
+/**
+ * Ensures an element with the given className exists, and then returns the
+ * first element with the provided className.
+ * @see {goog.dom.query}
+ * @param {string} className the name of the class to look for.
+ * @param {(!Element|!Document)=} opt_root Optional element or document to look
+ *     in.
+ * @return {!Element} The first item found with the class name provided.
+ * @throws {goog.asserts.AssertionError} Thrown if no element is found.
+ */
+goog.dom.DomHelper.prototype.getRequiredElementByClass = function(
+    className, opt_root) {
+  var root = opt_root || this.document_;
+  return goog.dom.getRequiredElementByClass(className, root);
+};
+
+
+/**
+ * Alias for {@code getElementsByTagNameAndClass}.
+ * @deprecated Use DomHelper getElementsByTagNameAndClass.
+ * @see goog.dom.query
+ *
+ * @param {(string|?goog.dom.TagName<T>)=} opt_tag Element tag name.
+ * @param {?string=} opt_class Optional class name.
+ * @param {Element=} opt_el Optional element to look in.
+ * @return {!IArrayLike<R>} Array-like list of elements (only a length property
+ *     and numerical indices are guaranteed to exist). The members of the array
+ *     are {!Element} if opt_tag is a string or more specific types if it is
+ *     a member of goog.dom.TagName (e.g. {!HTMLAnchorElement} for
+ *     goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ */
+goog.dom.DomHelper.prototype.$$ =
+    goog.dom.DomHelper.prototype.getElementsByTagNameAndClass;
+
+
+/**
+ * Sets a number of properties on a node.
+ * @param {Element} element DOM node to set properties on.
+ * @param {Object} properties Hash of property:value pairs.
+ */
+goog.dom.DomHelper.prototype.setProperties = goog.dom.setProperties;
+
+
+/**
+ * Gets the dimensions of the viewport.
+ * @param {Window=} opt_window Optional window element to test. Defaults to
+ *     the window of the Dom Helper.
+ * @return {!goog.math.Size} Object with values 'width' and 'height'.
+ */
+goog.dom.DomHelper.prototype.getViewportSize = function(opt_window) {
+  // TODO(arv): This should not take an argument. That breaks the rule of a
+  // a DomHelper representing a single frame/window/document.
+  return goog.dom.getViewportSize(opt_window || this.getWindow());
+};
+
+
+/**
+ * Calculates the height of the document.
+ *
+ * @return {number} The height of the document.
+ */
+goog.dom.DomHelper.prototype.getDocumentHeight = function() {
+  return goog.dom.getDocumentHeight_(this.getWindow());
+};
+
+
+/**
+ * Typedef for use with goog.dom.createDom and goog.dom.append.
+ * @typedef {Object|string|Array|NodeList}
+ */
+goog.dom.Appendable;
+
+
+/**
+ * Returns a dom node with a set of attributes.  This function accepts varargs
+ * for subsequent nodes to be added.  Subsequent nodes will be added to the
+ * first node as childNodes.
+ *
+ * So:
+ * <code>createDom(goog.dom.TagName.DIV, null, createDom(goog.dom.TagName.P),
+ * createDom(goog.dom.TagName.P));</code> would return a div with two child
+ * paragraphs
+ *
+ * An easy way to move all child nodes of an existing element to a new parent
+ * element is:
+ * <code>createDom(goog.dom.TagName.DIV, null, oldElement.childNodes);</code>
+ * which will remove all child nodes from the old element and add them as
+ * child nodes of the new DIV.
+ *
+ * @param {string|!goog.dom.TagName<T>} tagName Tag to create.
+ * @param {?Object|?Array<string>|string=} opt_attributes If object, then a map
+ *     of name-value pairs for attributes. If a string, then this is the
+ *     className of the new element. If an array, the elements will be joined
+ *     together as the className of the new element.
+ * @param {...goog.dom.Appendable} var_args Further DOM nodes or
+ *     strings for text nodes. If one of the var_args is an array or
+ *     NodeList, its elements will be added as childNodes instead.
+ * @return {R} Reference to a DOM node. The return type is {!Element} if tagName
+ *     is a string or a more specific type if it is a member of
+ *     goog.dom.TagName (e.g. {!HTMLAnchorElement} for goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ */
+goog.dom.DomHelper.prototype.createDom = function(
+    tagName, opt_attributes, var_args) {
+  return goog.dom.createDom_(this.document_, arguments);
+};
+
+
+/**
+ * Alias for {@code createDom}.
+ * @param {string|!goog.dom.TagName<T>} tagName Tag to create.
+ * @param {?Object|?Array<string>|string=} opt_attributes If object, then a map
+ *     of name-value pairs for attributes. If a string, then this is the
+ *     className of the new element. If an array, the elements will be joined
+ *     together as the className of the new element.
+ * @param {...goog.dom.Appendable} var_args Further DOM nodes or strings for
+ *     text nodes.  If one of the var_args is an array, its children will be
+ *     added as childNodes instead.
+ * @return {R} Reference to a DOM node. The return type is {!Element} if tagName
+ *     is a string or a more specific type if it is a member of
+ *     goog.dom.TagName (e.g. {!HTMLAnchorElement} for goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ * @deprecated Use {@link goog.dom.DomHelper.prototype.createDom} instead.
+ */
+goog.dom.DomHelper.prototype.$dom = goog.dom.DomHelper.prototype.createDom;
+
+
+/**
+ * Creates a new element.
+ * @param {string|!goog.dom.TagName<T>} name Tag to create.
+ * @return {R} The new element. The return type is {!Element} if name is
+ *     a string or a more specific type if it is a member of goog.dom.TagName
+ *     (e.g. {!HTMLAnchorElement} for goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ */
+goog.dom.DomHelper.prototype.createElement = function(name) {
+  return goog.dom.createElement_(this.document_, name);
+};
+
+
+/**
+ * Creates a new text node.
+ * @param {number|string} content Content.
+ * @return {!Text} The new text node.
+ */
+goog.dom.DomHelper.prototype.createTextNode = function(content) {
+  return this.document_.createTextNode(String(content));
+};
+
+
+/**
+ * Create a table.
+ * @param {number} rows The number of rows in the table.  Must be >= 1.
+ * @param {number} columns The number of columns in the table.  Must be >= 1.
+ * @param {boolean=} opt_fillWithNbsp If true, fills table entries with
+ *     {@code goog.string.Unicode.NBSP} characters.
+ * @return {!HTMLElement} The created table.
+ */
+goog.dom.DomHelper.prototype.createTable = function(
+    rows, columns, opt_fillWithNbsp) {
+  return goog.dom.createTable_(
+      this.document_, rows, columns, !!opt_fillWithNbsp);
+};
+
+
+/**
+ * Converts an HTML into a node or a document fragment. A single Node is used if
+ * {@code html} only generates a single node. If {@code html} generates multiple
+ * nodes then these are put inside a {@code DocumentFragment}. This is a safe
+ * version of {@code goog.dom.DomHelper#htmlToDocumentFragment} which is now
+ * deleted.
+ * @param {!goog.html.SafeHtml} html The HTML markup to convert.
+ * @return {!Node} The resulting node.
+ */
+goog.dom.DomHelper.prototype.safeHtmlToNode = function(html) {
+  return goog.dom.safeHtmlToNode_(this.document_, html);
+};
+
+
+/**
+ * Returns true if the browser is in "CSS1-compatible" (standards-compliant)
+ * mode, false otherwise.
+ * @return {boolean} True if in CSS1-compatible mode.
+ */
+goog.dom.DomHelper.prototype.isCss1CompatMode = function() {
+  return goog.dom.isCss1CompatMode_(this.document_);
+};
+
+
+/**
+ * Gets the window object associated with the document.
+ * @return {!Window} The window associated with the given document.
+ */
+goog.dom.DomHelper.prototype.getWindow = function() {
+  return goog.dom.getWindow_(this.document_);
+};
+
+
+/**
+ * Gets the document scroll element.
+ * @return {!Element} Scrolling element.
+ */
+goog.dom.DomHelper.prototype.getDocumentScrollElement = function() {
+  return goog.dom.getDocumentScrollElement_(this.document_);
+};
+
+
+/**
+ * Gets the document scroll distance as a coordinate object.
+ * @return {!goog.math.Coordinate} Object with properties 'x' and 'y'.
+ */
+goog.dom.DomHelper.prototype.getDocumentScroll = function() {
+  return goog.dom.getDocumentScroll_(this.document_);
+};
+
+
+/**
+ * Determines the active element in the given document.
+ * @param {Document=} opt_doc The document to look in.
+ * @return {Element} The active element.
+ */
+goog.dom.DomHelper.prototype.getActiveElement = function(opt_doc) {
+  return goog.dom.getActiveElement(opt_doc || this.document_);
+};
+
+
+/**
+ * Appends a child to a node.
+ * @param {Node} parent Parent.
+ * @param {Node} child Child.
+ */
+goog.dom.DomHelper.prototype.appendChild = goog.dom.appendChild;
+
+
+/**
+ * Appends a node with text or other nodes.
+ * @param {!Node} parent The node to append nodes to.
+ * @param {...goog.dom.Appendable} var_args The things to append to the node.
+ *     If this is a Node it is appended as is.
+ *     If this is a string then a text node is appended.
+ *     If this is an array like object then fields 0 to length - 1 are appended.
+ */
+goog.dom.DomHelper.prototype.append = goog.dom.append;
+
+
+/**
+ * Determines if the given node can contain children, intended to be used for
+ * HTML generation.
+ *
+ * @param {Node} node The node to check.
+ * @return {boolean} Whether the node can contain children.
+ */
+goog.dom.DomHelper.prototype.canHaveChildren = goog.dom.canHaveChildren;
+
+
+/**
+ * Removes all the child nodes on a DOM node.
+ * @param {Node} node Node to remove children from.
+ */
+goog.dom.DomHelper.prototype.removeChildren = goog.dom.removeChildren;
+
+
+/**
+ * Inserts a new node before an existing reference node (i.e., as the previous
+ * sibling). If the reference node has no parent, then does nothing.
+ * @param {Node} newNode Node to insert.
+ * @param {Node} refNode Reference node to insert before.
+ */
+goog.dom.DomHelper.prototype.insertSiblingBefore = goog.dom.insertSiblingBefore;
+
+
+/**
+ * Inserts a new node after an existing reference node (i.e., as the next
+ * sibling). If the reference node has no parent, then does nothing.
+ * @param {Node} newNode Node to insert.
+ * @param {Node} refNode Reference node to insert after.
+ */
+goog.dom.DomHelper.prototype.insertSiblingAfter = goog.dom.insertSiblingAfter;
+
+
+/**
+ * Insert a child at a given index. If index is larger than the number of child
+ * nodes that the parent currently has, the node is inserted as the last child
+ * node.
+ * @param {Element} parent The element into which to insert the child.
+ * @param {Node} child The element to insert.
+ * @param {number} index The index at which to insert the new child node. Must
+ *     not be negative.
+ */
+goog.dom.DomHelper.prototype.insertChildAt = goog.dom.insertChildAt;
+
+
+/**
+ * Removes a node from its parent.
+ * @param {Node} node The node to remove.
+ * @return {Node} The node removed if removed; else, null.
+ */
+goog.dom.DomHelper.prototype.removeNode = goog.dom.removeNode;
+
+
+/**
+ * Replaces a node in the DOM tree. Will do nothing if {@code oldNode} has no
+ * parent.
+ * @param {Node} newNode Node to insert.
+ * @param {Node} oldNode Node to replace.
+ */
+goog.dom.DomHelper.prototype.replaceNode = goog.dom.replaceNode;
+
+
+/**
+ * Flattens an element. That is, removes it and replace it with its children.
+ * @param {Element} element The element to flatten.
+ * @return {Element|undefined} The original element, detached from the document
+ *     tree, sans children, or undefined if the element was already not in the
+ *     document.
+ */
+goog.dom.DomHelper.prototype.flattenElement = goog.dom.flattenElement;
+
+
+/**
+ * Returns an array containing just the element children of the given element.
+ * @param {Element} element The element whose element children we want.
+ * @return {!(Array<!Element>|NodeList<!Element>)} An array or array-like list
+ *     of just the element children of the given element.
+ */
+goog.dom.DomHelper.prototype.getChildren = goog.dom.getChildren;
+
+
+/**
+ * Returns the first child node that is an element.
+ * @param {Node} node The node to get the first child element of.
+ * @return {Element} The first child node of {@code node} that is an element.
+ */
+goog.dom.DomHelper.prototype.getFirstElementChild =
+    goog.dom.getFirstElementChild;
+
+
+/**
+ * Returns the last child node that is an element.
+ * @param {Node} node The node to get the last child element of.
+ * @return {Element} The last child node of {@code node} that is an element.
+ */
+goog.dom.DomHelper.prototype.getLastElementChild = goog.dom.getLastElementChild;
+
+
+/**
+ * Returns the first next sibling that is an element.
+ * @param {Node} node The node to get the next sibling element of.
+ * @return {Element} The next sibling of {@code node} that is an element.
+ */
+goog.dom.DomHelper.prototype.getNextElementSibling =
+    goog.dom.getNextElementSibling;
+
+
+/**
+ * Returns the first previous sibling that is an element.
+ * @param {Node} node The node to get the previous sibling element of.
+ * @return {Element} The first previous sibling of {@code node} that is
+ *     an element.
+ */
+goog.dom.DomHelper.prototype.getPreviousElementSibling =
+    goog.dom.getPreviousElementSibling;
+
+
+/**
+ * Returns the next node in source order from the given node.
+ * @param {Node} node The node.
+ * @return {Node} The next node in the DOM tree, or null if this was the last
+ *     node.
+ */
+goog.dom.DomHelper.prototype.getNextNode = goog.dom.getNextNode;
+
+
+/**
+ * Returns the previous node in source order from the given node.
+ * @param {Node} node The node.
+ * @return {Node} The previous node in the DOM tree, or null if this was the
+ *     first node.
+ */
+goog.dom.DomHelper.prototype.getPreviousNode = goog.dom.getPreviousNode;
+
+
+/**
+ * Whether the object looks like a DOM node.
+ * @param {?} obj The object being tested for node likeness.
+ * @return {boolean} Whether the object looks like a DOM node.
+ */
+goog.dom.DomHelper.prototype.isNodeLike = goog.dom.isNodeLike;
+
+
+/**
+ * Whether the object looks like an Element.
+ * @param {?} obj The object being tested for Element likeness.
+ * @return {boolean} Whether the object looks like an Element.
+ */
+goog.dom.DomHelper.prototype.isElement = goog.dom.isElement;
+
+
+/**
+ * Returns true if the specified value is a Window object. This includes the
+ * global window for HTML pages, and iframe windows.
+ * @param {?} obj Variable to test.
+ * @return {boolean} Whether the variable is a window.
+ */
+goog.dom.DomHelper.prototype.isWindow = goog.dom.isWindow;
+
+
+/**
+ * Returns an element's parent, if it's an Element.
+ * @param {Element} element The DOM element.
+ * @return {Element} The parent, or null if not an Element.
+ */
+goog.dom.DomHelper.prototype.getParentElement = goog.dom.getParentElement;
+
+
+/**
+ * Whether a node contains another node.
+ * @param {Node} parent The node that should contain the other node.
+ * @param {Node} descendant The node to test presence of.
+ * @return {boolean} Whether the parent node contains the descendent node.
+ */
+goog.dom.DomHelper.prototype.contains = goog.dom.contains;
+
+
+/**
+ * Compares the document order of two nodes, returning 0 if they are the same
+ * node, a negative number if node1 is before node2, and a positive number if
+ * node2 is before node1.  Note that we compare the order the tags appear in the
+ * document so in the tree <b><i>text</i></b> the B node is considered to be
+ * before the I node.
+ *
+ * @param {Node} node1 The first node to compare.
+ * @param {Node} node2 The second node to compare.
+ * @return {number} 0 if the nodes are the same node, a negative number if node1
+ *     is before node2, and a positive number if node2 is before node1.
+ */
+goog.dom.DomHelper.prototype.compareNodeOrder = goog.dom.compareNodeOrder;
+
+
+/**
+ * Find the deepest common ancestor of the given nodes.
+ * @param {...Node} var_args The nodes to find a common ancestor of.
+ * @return {Node} The common ancestor of the nodes, or null if there is none.
+ *     null will only be returned if two or more of the nodes are from different
+ *     documents.
+ */
+goog.dom.DomHelper.prototype.findCommonAncestor = goog.dom.findCommonAncestor;
+
+
+/**
+ * Returns the owner document for a node.
+ * @param {Node} node The node to get the document for.
+ * @return {!Document} The document owning the node.
+ */
+goog.dom.DomHelper.prototype.getOwnerDocument = goog.dom.getOwnerDocument;
+
+
+/**
+ * Cross browser function for getting the document element of an iframe.
+ * @param {Element} iframe Iframe element.
+ * @return {!Document} The frame content document.
+ */
+goog.dom.DomHelper.prototype.getFrameContentDocument =
+    goog.dom.getFrameContentDocument;
+
+
+/**
+ * Cross browser function for getting the window of a frame or iframe.
+ * @param {Element} frame Frame element.
+ * @return {Window} The window associated with the given frame.
+ */
+goog.dom.DomHelper.prototype.getFrameContentWindow =
+    goog.dom.getFrameContentWindow;
+
+
+/**
+ * Sets the text content of a node, with cross-browser support.
+ * @param {Node} node The node to change the text content of.
+ * @param {string|number} text The value that should replace the node's content.
+ */
+goog.dom.DomHelper.prototype.setTextContent = goog.dom.setTextContent;
+
+
+/**
+ * Gets the outerHTML of a node, which islike innerHTML, except that it
+ * actually contains the HTML of the node itself.
+ * @param {Element} element The element to get the HTML of.
+ * @return {string} The outerHTML of the given element.
+ */
+goog.dom.DomHelper.prototype.getOuterHtml = goog.dom.getOuterHtml;
+
+
+/**
+ * Finds the first descendant node that matches the filter function. This does
+ * a depth first search.
+ * @param {Node} root The root of the tree to search.
+ * @param {function(Node) : boolean} p The filter function.
+ * @return {Node|undefined} The found node or undefined if none is found.
+ */
+goog.dom.DomHelper.prototype.findNode = goog.dom.findNode;
+
+
+/**
+ * Finds all the descendant nodes that matches the filter function. This does a
+ * depth first search.
+ * @param {Node} root The root of the tree to search.
+ * @param {function(Node) : boolean} p The filter function.
+ * @return {Array<Node>} The found nodes or an empty array if none are found.
+ */
+goog.dom.DomHelper.prototype.findNodes = goog.dom.findNodes;
+
+
+/**
+ * Returns true if the element has a tab index that allows it to receive
+ * keyboard focus (tabIndex >= 0), false otherwise.  Note that some elements
+ * natively support keyboard focus, even if they have no tab index.
+ * @param {!Element} element Element to check.
+ * @return {boolean} Whether the element has a tab index that allows keyboard
+ *     focus.
+ */
+goog.dom.DomHelper.prototype.isFocusableTabIndex = goog.dom.isFocusableTabIndex;
+
+
+/**
+ * Enables or disables keyboard focus support on the element via its tab index.
+ * Only elements for which {@link goog.dom.isFocusableTabIndex} returns true
+ * (or elements that natively support keyboard focus, like form elements) can
+ * receive keyboard focus.  See http://go/tabindex for more info.
+ * @param {Element} element Element whose tab index is to be changed.
+ * @param {boolean} enable Whether to set or remove a tab index on the element
+ *     that supports keyboard focus.
+ */
+goog.dom.DomHelper.prototype.setFocusableTabIndex =
+    goog.dom.setFocusableTabIndex;
+
+
+/**
+ * Returns true if the element can be focused, i.e. it has a tab index that
+ * allows it to receive keyboard focus (tabIndex >= 0), or it is an element
+ * that natively supports keyboard focus.
+ * @param {!Element} element Element to check.
+ * @return {boolean} Whether the element allows keyboard focus.
+ */
+goog.dom.DomHelper.prototype.isFocusable = goog.dom.isFocusable;
+
+
+/**
+ * Returns the text contents of the current node, without markup. New lines are
+ * stripped and whitespace is collapsed, such that each character would be
+ * visible.
+ *
+ * In browsers that support it, innerText is used.  Other browsers attempt to
+ * simulate it via node traversal.  Line breaks are canonicalized in IE.
+ *
+ * @param {Node} node The node from which we are getting content.
+ * @return {string} The text content.
+ */
+goog.dom.DomHelper.prototype.getTextContent = goog.dom.getTextContent;
+
+
+/**
+ * Returns the text length of the text contained in a node, without markup. This
+ * is equivalent to the selection length if the node was selected, or the number
+ * of cursor movements to traverse the node. Images & BRs take one space.  New
+ * lines are ignored.
+ *
+ * @param {Node} node The node whose text content length is being calculated.
+ * @return {number} The length of {@code node}'s text content.
+ */
+goog.dom.DomHelper.prototype.getNodeTextLength = goog.dom.getNodeTextLength;
+
+
+/**
+ * Returns the text offset of a node relative to one of its ancestors. The text
+ * length is the same as the length calculated by
+ * {@code goog.dom.getNodeTextLength}.
+ *
+ * @param {Node} node The node whose offset is being calculated.
+ * @param {Node=} opt_offsetParent Defaults to the node's owner document's body.
+ * @return {number} The text offset.
+ */
+goog.dom.DomHelper.prototype.getNodeTextOffset = goog.dom.getNodeTextOffset;
+
+
+/**
+ * Returns the node at a given offset in a parent node.  If an object is
+ * provided for the optional third parameter, the node and the remainder of the
+ * offset will stored as properties of this object.
+ * @param {Node} parent The parent node.
+ * @param {number} offset The offset into the parent node.
+ * @param {Object=} opt_result Object to be used to store the return value. The
+ *     return value will be stored in the form {node: Node, remainder: number}
+ *     if this object is provided.
+ * @return {Node} The node at the given offset.
+ */
+goog.dom.DomHelper.prototype.getNodeAtOffset = goog.dom.getNodeAtOffset;
+
+
+/**
+ * Returns true if the object is a {@code NodeList}.  To qualify as a NodeList,
+ * the object must have a numeric length property and an item function (which
+ * has type 'string' on IE for some reason).
+ * @param {Object} val Object to test.
+ * @return {boolean} Whether the object is a NodeList.
+ */
+goog.dom.DomHelper.prototype.isNodeList = goog.dom.isNodeList;
+
+
+/**
+ * Walks up the DOM hierarchy returning the first ancestor that has the passed
+ * tag name and/or class name. If the passed element matches the specified
+ * criteria, the element itself is returned.
+ * @param {Node} element The DOM node to start with.
+ * @param {?(goog.dom.TagName<T>|string)=} opt_tag The tag name to match (or
+ *     null/undefined to match only based on class name).
+ * @param {?string=} opt_class The class name to match (or null/undefined to
+ *     match only based on tag name).
+ * @param {number=} opt_maxSearchSteps Maximum number of levels to search up the
+ *     dom.
+ * @return {?R} The first ancestor that matches the passed criteria, or
+ *     null if no match is found. The return type is {?Element} if opt_tag is
+ *     not a member of goog.dom.TagName or a more specific type if it is (e.g.
+ *     {?HTMLAnchorElement} for goog.dom.TagName.A).
+ * @template T
+ * @template R := cond(isUnknown(T), 'Element', T) =:
+ */
+goog.dom.DomHelper.prototype.getAncestorByTagNameAndClass =
+    goog.dom.getAncestorByTagNameAndClass;
+
+
+/**
+ * Walks up the DOM hierarchy returning the first ancestor that has the passed
+ * class name. If the passed element matches the specified criteria, the
+ * element itself is returned.
+ * @param {Node} element The DOM node to start with.
+ * @param {string} class The class name to match.
+ * @param {number=} opt_maxSearchSteps Maximum number of levels to search up the
+ *     dom.
+ * @return {Element} The first ancestor that matches the passed criteria, or
+ *     null if none match.
+ */
+goog.dom.DomHelper.prototype.getAncestorByClass = goog.dom.getAncestorByClass;
+
+
+/**
+ * Walks up the DOM hierarchy returning the first ancestor that passes the
+ * matcher function.
+ * @param {Node} element The DOM node to start with.
+ * @param {function(Node) : boolean} matcher A function that returns true if the
+ *     passed node matches the desired criteria.
+ * @param {boolean=} opt_includeNode If true, the node itself is included in
+ *     the search (the first call to the matcher will pass startElement as
+ *     the node to test).
+ * @param {number=} opt_maxSearchSteps Maximum number of levels to search up the
+ *     dom.
+ * @return {Node} DOM node that matched the matcher, or null if there was
+ *     no match.
+ */
+goog.dom.DomHelper.prototype.getAncestor = goog.dom.getAncestor;
+
+
+/**
+ * Gets '2d' context of a canvas. Shortcut for canvas.getContext('2d') with a
+ * type information.
+ * @param {!HTMLCanvasElement} canvas
+ * @return {!CanvasRenderingContext2D}
+ */
+goog.dom.DomHelper.prototype.getCanvasContext2D = goog.dom.getCanvasContext2D;
diff --git a/third_party/ink/closure/dom/htmlelement.js b/third_party/ink/closure/dom/htmlelement.js
new file mode 100644
index 0000000..c48f753
--- /dev/null
+++ b/third_party/ink/closure/dom/htmlelement.js
@@ -0,0 +1,29 @@
+// Copyright 2017 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.
+
+goog.provide('goog.dom.HtmlElement');
+
+
+
+/**
+ * This subclass of HTMLElement is used when only a HTMLElement is possible and
+ * not any of its subclasses. Normally, a type can refer to an instance of
+ * itself or an instance of any subtype. More concretely, if HTMLElement is used
+ * then the compiler must assume that it might still be e.g. HTMLScriptElement.
+ * With this, the type check knows that it couldn't be any special element.
+ *
+ * @constructor
+ * @extends {HTMLElement}
+ */
+goog.dom.HtmlElement = function() {};
diff --git a/third_party/ink/closure/dom/nodetype.js b/third_party/ink/closure/dom/nodetype.js
new file mode 100644
index 0000000..cccb470
--- /dev/null
+++ b/third_party/ink/closure/dom/nodetype.js
@@ -0,0 +1,48 @@
+// Copyright 2006 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 Definition of goog.dom.NodeType.
+ */
+
+goog.provide('goog.dom.NodeType');
+
+
+/**
+ * Constants for the nodeType attribute in the Node interface.
+ *
+ * These constants match those specified in the Node interface. These are
+ * usually present on the Node object in recent browsers, but not in older
+ * browsers (specifically, early IEs) and thus are given here.
+ *
+ * In some browsers (early IEs), these are not defined on the Node object,
+ * so they are provided here.
+ *
+ * See http://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-1950641247
+ * @enum {number}
+ */
+goog.dom.NodeType = {
+  ELEMENT: 1,
+  ATTRIBUTE: 2,
+  TEXT: 3,
+  CDATA_SECTION: 4,
+  ENTITY_REFERENCE: 5,
+  ENTITY: 6,
+  PROCESSING_INSTRUCTION: 7,
+  COMMENT: 8,
+  DOCUMENT: 9,
+  DOCUMENT_TYPE: 10,
+  DOCUMENT_FRAGMENT: 11,
+  NOTATION: 12
+};
diff --git a/third_party/ink/closure/dom/safe.js b/third_party/ink/closure/dom/safe.js
new file mode 100644
index 0000000..a869e1b
--- /dev/null
+++ b/third_party/ink/closure/dom/safe.js
@@ -0,0 +1,458 @@
+// Copyright 2013 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 Type-safe wrappers for unsafe DOM APIs.
+ *
+ * This file provides type-safe wrappers for DOM APIs that can result in
+ * cross-site scripting (XSS) vulnerabilities, if the API is supplied with
+ * untrusted (attacker-controlled) input.  Instead of plain strings, the type
+ * safe wrappers consume values of types from the goog.html package whose
+ * contract promises that values are safe to use in the corresponding context.
+ *
+ * Hence, a program that exclusively uses the wrappers in this file (i.e., whose
+ * only reference to security-sensitive raw DOM APIs are in this file) is
+ * guaranteed to be free of XSS due to incorrect use of such DOM APIs (modulo
+ * correctness of code that produces values of the respective goog.html types,
+ * and absent code that violates type safety).
+ *
+ * For example, assigning to an element's .innerHTML property a string that is
+ * derived (even partially) from untrusted input typically results in an XSS
+ * vulnerability. The type-safe wrapper goog.dom.safe.setInnerHtml consumes a
+ * value of type goog.html.SafeHtml, whose contract states that using its values
+ * in a HTML context will not result in XSS. Hence a program that is free of
+ * direct assignments to any element's innerHTML property (with the exception of
+ * the assignment to .innerHTML in this file) is guaranteed to be free of XSS
+ * due to assignment of untrusted strings to the innerHTML property.
+ */
+
+goog.provide('goog.dom.safe');
+goog.provide('goog.dom.safe.InsertAdjacentHtmlPosition');
+
+goog.require('goog.asserts');
+goog.require('goog.dom.asserts');
+goog.require('goog.html.SafeHtml');
+goog.require('goog.html.SafeScript');
+goog.require('goog.html.SafeStyle');
+goog.require('goog.html.SafeUrl');
+goog.require('goog.html.TrustedResourceUrl');
+goog.require('goog.string');
+goog.require('goog.string.Const');
+
+
+/** @enum {string} */
+goog.dom.safe.InsertAdjacentHtmlPosition = {
+  AFTERBEGIN: 'afterbegin',
+  AFTEREND: 'afterend',
+  BEFOREBEGIN: 'beforebegin',
+  BEFOREEND: 'beforeend'
+};
+
+
+/**
+ * Inserts known-safe HTML into a Node, at the specified position.
+ * @param {!Node} node The node on which to call insertAdjacentHTML.
+ * @param {!goog.dom.safe.InsertAdjacentHtmlPosition} position Position where
+ *     to insert the HTML.
+ * @param {!goog.html.SafeHtml} html The known-safe HTML to insert.
+ */
+goog.dom.safe.insertAdjacentHtml = function(node, position, html) {
+  node.insertAdjacentHTML(position, goog.html.SafeHtml.unwrap(html));
+};
+
+
+/**
+ * Tags not allowed in goog.dom.safe.setInnerHtml.
+ * @private @const {!Object<string, boolean>}
+ */
+goog.dom.safe.SET_INNER_HTML_DISALLOWED_TAGS_ = {
+  'MATH': true,
+  'SCRIPT': true,
+  'STYLE': true,
+  'SVG': true,
+  'TEMPLATE': true
+};
+
+
+/**
+ * Assigns known-safe HTML to an element's innerHTML property.
+ * @param {!Element} elem The element whose innerHTML is to be assigned to.
+ * @param {!goog.html.SafeHtml} html The known-safe HTML to assign.
+ * @throws {Error} If called with one of these tags: math, script, style, svg,
+ *     template.
+ */
+goog.dom.safe.setInnerHtml = function(elem, html) {
+  if (goog.asserts.ENABLE_ASSERTS) {
+    var tagName = elem.tagName.toUpperCase();
+    if (goog.dom.safe.SET_INNER_HTML_DISALLOWED_TAGS_[tagName]) {
+      throw new Error(
+          'goog.dom.safe.setInnerHtml cannot be used to set content of ' +
+          elem.tagName + '.');
+    }
+  }
+  elem.innerHTML = goog.html.SafeHtml.unwrap(html);
+};
+
+
+/**
+ * Assigns known-safe HTML to an element's outerHTML property.
+ * @param {!Element} elem The element whose outerHTML is to be assigned to.
+ * @param {!goog.html.SafeHtml} html The known-safe HTML to assign.
+ */
+goog.dom.safe.setOuterHtml = function(elem, html) {
+  elem.outerHTML = goog.html.SafeHtml.unwrap(html);
+};
+
+
+/**
+ * Sets the given element's style property to the contents of the provided
+ * SafeStyle object.
+ * @param {!Element} elem
+ * @param {!goog.html.SafeStyle} style
+ */
+goog.dom.safe.setStyle = function(elem, style) {
+  elem.style.cssText = goog.html.SafeStyle.unwrap(style);
+};
+
+
+/**
+ * Writes known-safe HTML to a document.
+ * @param {!Document} doc The document to be written to.
+ * @param {!goog.html.SafeHtml} html The known-safe HTML to assign.
+ */
+goog.dom.safe.documentWrite = function(doc, html) {
+  doc.write(goog.html.SafeHtml.unwrap(html));
+};
+
+
+/**
+ * Safely assigns a URL to an anchor element's href property.
+ *
+ * If url is of type goog.html.SafeUrl, its value is unwrapped and assigned to
+ * anchor's href property.  If url is of type string however, it is first
+ * sanitized using goog.html.SafeUrl.sanitize.
+ *
+ * Example usage:
+ *   goog.dom.safe.setAnchorHref(anchorEl, url);
+ * which is a safe alternative to
+ *   anchorEl.href = url;
+ * The latter can result in XSS vulnerabilities if url is a
+ * user-/attacker-controlled value.
+ *
+ * @param {!HTMLAnchorElement} anchor The anchor element whose href property
+ *     is to be assigned to.
+ * @param {string|!goog.html.SafeUrl} url The URL to assign.
+ * @see goog.html.SafeUrl#sanitize
+ */
+goog.dom.safe.setAnchorHref = function(anchor, url) {
+  goog.dom.asserts.assertIsHTMLAnchorElement(anchor);
+  /** @type {!goog.html.SafeUrl} */
+  var safeUrl;
+  if (url instanceof goog.html.SafeUrl) {
+    safeUrl = url;
+  } else {
+    safeUrl = goog.html.SafeUrl.sanitizeAssertUnchanged(url);
+  }
+  anchor.href = goog.html.SafeUrl.unwrap(safeUrl);
+};
+
+
+/**
+ * Safely assigns a URL to an image element's src property.
+ *
+ * If url is of type goog.html.SafeUrl, its value is unwrapped and assigned to
+ * image's src property.  If url is of type string however, it is first
+ * sanitized using goog.html.SafeUrl.sanitize.
+ *
+ * @param {!HTMLImageElement} imageElement The image element whose src property
+ *     is to be assigned to.
+ * @param {string|!goog.html.SafeUrl} url The URL to assign.
+ * @see goog.html.SafeUrl#sanitize
+ */
+goog.dom.safe.setImageSrc = function(imageElement, url) {
+  goog.dom.asserts.assertIsHTMLImageElement(imageElement);
+  /** @type {!goog.html.SafeUrl} */
+  var safeUrl;
+  if (url instanceof goog.html.SafeUrl) {
+    safeUrl = url;
+  } else {
+    safeUrl = goog.html.SafeUrl.sanitizeAssertUnchanged(url);
+  }
+  imageElement.src = goog.html.SafeUrl.unwrap(safeUrl);
+};
+
+
+/**
+ * Safely assigns a URL to an embed element's src property.
+ *
+ * Example usage:
+ *   goog.dom.safe.setEmbedSrc(embedEl, url);
+ * which is a safe alternative to
+ *   embedEl.src = url;
+ * The latter can result in loading untrusted code unless it is ensured that
+ * the URL refers to a trustworthy resource.
+ *
+ * @param {!HTMLEmbedElement} embed The embed element whose src property
+ *     is to be assigned to.
+ * @param {!goog.html.TrustedResourceUrl} url The URL to assign.
+ */
+goog.dom.safe.setEmbedSrc = function(embed, url) {
+  goog.dom.asserts.assertIsHTMLEmbedElement(embed);
+  embed.src = goog.html.TrustedResourceUrl.unwrap(url);
+};
+
+
+/**
+ * Safely assigns a URL to a frame element's src property.
+ *
+ * Example usage:
+ *   goog.dom.safe.setFrameSrc(frameEl, url);
+ * which is a safe alternative to
+ *   frameEl.src = url;
+ * The latter can result in loading untrusted code unless it is ensured that
+ * the URL refers to a trustworthy resource.
+ *
+ * @param {!HTMLFrameElement} frame The frame element whose src property
+ *     is to be assigned to.
+ * @param {!goog.html.TrustedResourceUrl} url The URL to assign.
+ */
+goog.dom.safe.setFrameSrc = function(frame, url) {
+  goog.dom.asserts.assertIsHTMLFrameElement(frame);
+  frame.src = goog.html.TrustedResourceUrl.unwrap(url);
+};
+
+
+/**
+ * Safely assigns a URL to an iframe element's src property.
+ *
+ * Example usage:
+ *   goog.dom.safe.setIframeSrc(iframeEl, url);
+ * which is a safe alternative to
+ *   iframeEl.src = url;
+ * The latter can result in loading untrusted code unless it is ensured that
+ * the URL refers to a trustworthy resource.
+ *
+ * @param {!HTMLIFrameElement} iframe The iframe element whose src property
+ *     is to be assigned to.
+ * @param {!goog.html.TrustedResourceUrl} url The URL to assign.
+ */
+goog.dom.safe.setIframeSrc = function(iframe, url) {
+  goog.dom.asserts.assertIsHTMLIFrameElement(iframe);
+  iframe.src = goog.html.TrustedResourceUrl.unwrap(url);
+};
+
+
+/**
+ * Safely assigns HTML to an iframe element's srcdoc property.
+ *
+ * Example usage:
+ *   goog.dom.safe.setIframeSrcdoc(iframeEl, safeHtml);
+ * which is a safe alternative to
+ *   iframeEl.srcdoc = html;
+ * The latter can result in loading untrusted code.
+ *
+ * @param {!HTMLIFrameElement} iframe The iframe element whose srcdoc property
+ *     is to be assigned to.
+ * @param {!goog.html.SafeHtml} html The HTML to assign.
+ */
+goog.dom.safe.setIframeSrcdoc = function(iframe, html) {
+  goog.dom.asserts.assertIsHTMLIFrameElement(iframe);
+  iframe.srcdoc = goog.html.SafeHtml.unwrap(html);
+};
+
+
+/**
+ * Safely sets a link element's href and rel properties. Whether or not
+ * the URL assigned to href has to be a goog.html.TrustedResourceUrl
+ * depends on the value of the rel property. If rel contains "stylesheet"
+ * then a TrustedResourceUrl is required.
+ *
+ * Example usage:
+ *   goog.dom.safe.setLinkHrefAndRel(linkEl, url, 'stylesheet');
+ * which is a safe alternative to
+ *   linkEl.rel = 'stylesheet';
+ *   linkEl.href = url;
+ * The latter can result in loading untrusted code unless it is ensured that
+ * the URL refers to a trustworthy resource.
+ *
+ * @param {!HTMLLinkElement} link The link element whose href property
+ *     is to be assigned to.
+ * @param {string|!goog.html.SafeUrl|!goog.html.TrustedResourceUrl} url The URL
+ *     to assign to the href property. Must be a TrustedResourceUrl if the
+ *     value assigned to rel contains "stylesheet". A string value is
+ *     sanitized with goog.html.SafeUrl.sanitize.
+ * @param {string} rel The value to assign to the rel property.
+ * @throws {Error} if rel contains "stylesheet" and url is not a
+ *     TrustedResourceUrl
+ * @see goog.html.SafeUrl#sanitize
+ */
+goog.dom.safe.setLinkHrefAndRel = function(link, url, rel) {
+  goog.dom.asserts.assertIsHTMLLinkElement(link);
+  link.rel = rel;
+  if (goog.string.caseInsensitiveContains(rel, 'stylesheet')) {
+    goog.asserts.assert(
+        url instanceof goog.html.TrustedResourceUrl,
+        'URL must be TrustedResourceUrl because "rel" contains "stylesheet"');
+    link.href = goog.html.TrustedResourceUrl.unwrap(url);
+  } else if (url instanceof goog.html.TrustedResourceUrl) {
+    link.href = goog.html.TrustedResourceUrl.unwrap(url);
+  } else if (url instanceof goog.html.SafeUrl) {
+    link.href = goog.html.SafeUrl.unwrap(url);
+  } else {  // string
+    // SafeUrl.sanitize must return legitimate SafeUrl when passed a string.
+    link.href =
+        goog.html.SafeUrl.sanitizeAssertUnchanged(url).getTypedStringValue();
+  }
+};
+
+
+/**
+ * Safely assigns a URL to an object element's data property.
+ *
+ * Example usage:
+ *   goog.dom.safe.setObjectData(objectEl, url);
+ * which is a safe alternative to
+ *   objectEl.data = url;
+ * The latter can result in loading untrusted code unless setit is ensured that
+ * the URL refers to a trustworthy resource.
+ *
+ * @param {!HTMLObjectElement} object The object element whose data property
+ *     is to be assigned to.
+ * @param {!goog.html.TrustedResourceUrl} url The URL to assign.
+ */
+goog.dom.safe.setObjectData = function(object, url) {
+  goog.dom.asserts.assertIsHTMLObjectElement(object);
+  object.data = goog.html.TrustedResourceUrl.unwrap(url);
+};
+
+
+/**
+ * Safely assigns a URL to a script element's src property.
+ *
+ * Example usage:
+ *   goog.dom.safe.setScriptSrc(scriptEl, url);
+ * which is a safe alternative to
+ *   scriptEl.src = url;
+ * The latter can result in loading untrusted code unless it is ensured that
+ * the URL refers to a trustworthy resource.
+ *
+ * @param {!HTMLScriptElement} script The script element whose src property
+ *     is to be assigned to.
+ * @param {!goog.html.TrustedResourceUrl} url The URL to assign.
+ */
+goog.dom.safe.setScriptSrc = function(script, url) {
+  goog.dom.asserts.assertIsHTMLScriptElement(script);
+  script.src = goog.html.TrustedResourceUrl.unwrap(url);
+};
+
+
+/**
+ * Safely assigns a value to a script element's content.
+ *
+ * Example usage:
+ *   goog.dom.safe.setScriptContent(scriptEl, content);
+ * which is a safe alternative to
+ *   scriptEl.text = content;
+ * The latter can result in executing untrusted code unless it is ensured that
+ * the code is loaded from a trustworthy resource.
+ *
+ * @param {!HTMLScriptElement} script The script element whose content is being
+ *     set.
+ * @param {!goog.html.SafeScript} content The content to assign.
+ */
+goog.dom.safe.setScriptContent = function(script, content) {
+  goog.dom.asserts.assertIsHTMLScriptElement(script);
+  script.text = goog.html.SafeScript.unwrap(content);
+};
+
+
+/**
+ * Safely assigns a URL to a Location object's href property.
+ *
+ * If url is of type goog.html.SafeUrl, its value is unwrapped and assigned to
+ * loc's href property.  If url is of type string however, it is first sanitized
+ * using goog.html.SafeUrl.sanitize.
+ *
+ * Example usage:
+ *   goog.dom.safe.setLocationHref(document.location, redirectUrl);
+ * which is a safe alternative to
+ *   document.location.href = redirectUrl;
+ * The latter can result in XSS vulnerabilities if redirectUrl is a
+ * user-/attacker-controlled value.
+ *
+ * @param {!Location} loc The Location object whose href property is to be
+ *     assigned to.
+ * @param {string|!goog.html.SafeUrl} url The URL to assign.
+ * @see goog.html.SafeUrl#sanitize
+ */
+goog.dom.safe.setLocationHref = function(loc, url) {
+  goog.dom.asserts.assertIsLocation(loc);
+  /** @type {!goog.html.SafeUrl} */
+  var safeUrl;
+  if (url instanceof goog.html.SafeUrl) {
+    safeUrl = url;
+  } else {
+    safeUrl = goog.html.SafeUrl.sanitizeAssertUnchanged(url);
+  }
+  loc.href = goog.html.SafeUrl.unwrap(safeUrl);
+};
+
+
+/**
+ * Safely opens a URL in a new window (via window.open).
+ *
+ * If url is of type goog.html.SafeUrl, its value is unwrapped and passed in to
+ * window.open.  If url is of type string however, it is first sanitized
+ * using goog.html.SafeUrl.sanitize.
+ *
+ * Note that this function does not prevent leakages via the referer that is
+ * sent by window.open. It is advised to only use this to open 1st party URLs.
+ *
+ * Example usage:
+ *   goog.dom.safe.openInWindow(url);
+ * which is a safe alternative to
+ *   window.open(url);
+ * The latter can result in XSS vulnerabilities if redirectUrl is a
+ * user-/attacker-controlled value.
+ *
+ * @param {string|!goog.html.SafeUrl} url The URL to open.
+ * @param {Window=} opt_openerWin Window of which to call the .open() method.
+ *     Defaults to the global window.
+ * @param {!goog.string.Const=} opt_name Name of the window to open in. Can be
+ *     _top, etc as allowed by window.open().
+ * @param {string=} opt_specs Comma-separated list of specifications, same as
+ *     in window.open().
+ * @param {boolean=} opt_replace Whether to replace the current entry in browser
+ *     history, same as in window.open().
+ * @return {Window} Window the url was opened in.
+ */
+goog.dom.safe.openInWindow = function(
+    url, opt_openerWin, opt_name, opt_specs, opt_replace) {
+  /** @type {!goog.html.SafeUrl} */
+  var safeUrl;
+  if (url instanceof goog.html.SafeUrl) {
+    safeUrl = url;
+  } else {
+    safeUrl = goog.html.SafeUrl.sanitizeAssertUnchanged(url);
+  }
+  var win = opt_openerWin || window;
+  return win.open(
+      goog.html.SafeUrl.unwrap(safeUrl),
+      // If opt_name is undefined, simply passing that in to open() causes IE to
+      // reuse the current window instead of opening a new one. Thus we pass ''
+      // in instead, which according to spec opens a new window. See
+      // https://html.spec.whatwg.org/multipage/browsers.html#dom-open .
+      opt_name ? goog.string.Const.unwrap(opt_name) : '', opt_specs,
+      opt_replace);
+};
diff --git a/third_party/ink/closure/dom/tagname.js b/third_party/ink/closure/dom/tagname.js
new file mode 100644
index 0000000..b3808ad1
--- /dev/null
+++ b/third_party/ink/closure/dom/tagname.js
@@ -0,0 +1,562 @@
+// Copyright 2007 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 Defines the goog.dom.TagName class. Its constants enumerate
+ * all HTML tag names specified in either the the W3C HTML 4.01 index of
+ * elements or the HTML5 draft specification.
+ *
+ * References:
+ * http://www.w3.org/TR/html401/index/elements.html
+ * http://dev.w3.org/html5/spec/section-index.html
+ */
+goog.provide('goog.dom.TagName');
+
+goog.require('goog.dom.HtmlElement');
+
+
+/**
+ * A tag name with the type of the element stored in the generic.
+ * @param {string} tagName
+ * @constructor
+ * @template T
+ */
+goog.dom.TagName = function(tagName) {
+  /** @private {string} */
+  this.tagName_ = tagName;
+};
+
+
+/**
+ * Returns the tag name.
+ * @return {string}
+ * @override
+ */
+goog.dom.TagName.prototype.toString = function() {
+  return this.tagName_;
+};
+
+
+// Closure Compiler unconditionally converts the following constants to their
+// string value (goog.dom.TagName.A -> 'A'). These are the consequences:
+// 1. Don't add any members or static members to goog.dom.TagName as they
+//    couldn't be accessed after this optimization.
+// 2. Keep the constant name and its string value the same:
+//    goog.dom.TagName.X = new goog.dom.TagName('Y');
+//    is converted to 'X', not 'Y'.
+
+
+/** @type {!goog.dom.TagName<!HTMLAnchorElement>} */
+goog.dom.TagName.A = new goog.dom.TagName('A');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.ABBR = new goog.dom.TagName('ABBR');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.ACRONYM = new goog.dom.TagName('ACRONYM');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.ADDRESS = new goog.dom.TagName('ADDRESS');
+
+
+/** @type {!goog.dom.TagName<!HTMLAppletElement>} */
+goog.dom.TagName.APPLET = new goog.dom.TagName('APPLET');
+
+
+/** @type {!goog.dom.TagName<!HTMLAreaElement>} */
+goog.dom.TagName.AREA = new goog.dom.TagName('AREA');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.ARTICLE = new goog.dom.TagName('ARTICLE');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.ASIDE = new goog.dom.TagName('ASIDE');
+
+
+/** @type {!goog.dom.TagName<!HTMLAudioElement>} */
+goog.dom.TagName.AUDIO = new goog.dom.TagName('AUDIO');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.B = new goog.dom.TagName('B');
+
+
+/** @type {!goog.dom.TagName<!HTMLBaseElement>} */
+goog.dom.TagName.BASE = new goog.dom.TagName('BASE');
+
+
+/** @type {!goog.dom.TagName<!HTMLBaseFontElement>} */
+goog.dom.TagName.BASEFONT = new goog.dom.TagName('BASEFONT');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.BDI = new goog.dom.TagName('BDI');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.BDO = new goog.dom.TagName('BDO');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.BIG = new goog.dom.TagName('BIG');
+
+
+/** @type {!goog.dom.TagName<!HTMLQuoteElement>} */
+goog.dom.TagName.BLOCKQUOTE = new goog.dom.TagName('BLOCKQUOTE');
+
+
+/** @type {!goog.dom.TagName<!HTMLBodyElement>} */
+goog.dom.TagName.BODY = new goog.dom.TagName('BODY');
+
+
+/** @type {!goog.dom.TagName<!HTMLBRElement>} */
+goog.dom.TagName.BR = new goog.dom.TagName('BR');
+
+
+/** @type {!goog.dom.TagName<!HTMLButtonElement>} */
+goog.dom.TagName.BUTTON = new goog.dom.TagName('BUTTON');
+
+
+/** @type {!goog.dom.TagName<!HTMLCanvasElement>} */
+goog.dom.TagName.CANVAS = new goog.dom.TagName('CANVAS');
+
+
+/** @type {!goog.dom.TagName<!HTMLTableCaptionElement>} */
+goog.dom.TagName.CAPTION = new goog.dom.TagName('CAPTION');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.CENTER = new goog.dom.TagName('CENTER');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.CITE = new goog.dom.TagName('CITE');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.CODE = new goog.dom.TagName('CODE');
+
+
+/** @type {!goog.dom.TagName<!HTMLTableColElement>} */
+goog.dom.TagName.COL = new goog.dom.TagName('COL');
+
+
+/** @type {!goog.dom.TagName<!HTMLTableColElement>} */
+goog.dom.TagName.COLGROUP = new goog.dom.TagName('COLGROUP');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.COMMAND = new goog.dom.TagName('COMMAND');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.DATA = new goog.dom.TagName('DATA');
+
+
+/** @type {!goog.dom.TagName<!HTMLDataListElement>} */
+goog.dom.TagName.DATALIST = new goog.dom.TagName('DATALIST');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.DD = new goog.dom.TagName('DD');
+
+
+/** @type {!goog.dom.TagName<!HTMLModElement>} */
+goog.dom.TagName.DEL = new goog.dom.TagName('DEL');
+
+
+/** @type {!goog.dom.TagName<!HTMLDetailsElement>} */
+goog.dom.TagName.DETAILS = new goog.dom.TagName('DETAILS');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.DFN = new goog.dom.TagName('DFN');
+
+
+/** @type {!goog.dom.TagName<!HTMLDialogElement>} */
+goog.dom.TagName.DIALOG = new goog.dom.TagName('DIALOG');
+
+
+/** @type {!goog.dom.TagName<!HTMLDirectoryElement>} */
+goog.dom.TagName.DIR = new goog.dom.TagName('DIR');
+
+
+/** @type {!goog.dom.TagName<!HTMLDivElement>} */
+goog.dom.TagName.DIV = new goog.dom.TagName('DIV');
+
+
+/** @type {!goog.dom.TagName<!HTMLDListElement>} */
+goog.dom.TagName.DL = new goog.dom.TagName('DL');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.DT = new goog.dom.TagName('DT');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.EM = new goog.dom.TagName('EM');
+
+
+/** @type {!goog.dom.TagName<!HTMLEmbedElement>} */
+goog.dom.TagName.EMBED = new goog.dom.TagName('EMBED');
+
+
+/** @type {!goog.dom.TagName<!HTMLFieldSetElement>} */
+goog.dom.TagName.FIELDSET = new goog.dom.TagName('FIELDSET');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.FIGCAPTION = new goog.dom.TagName('FIGCAPTION');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.FIGURE = new goog.dom.TagName('FIGURE');
+
+
+/** @type {!goog.dom.TagName<!HTMLFontElement>} */
+goog.dom.TagName.FONT = new goog.dom.TagName('FONT');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.FOOTER = new goog.dom.TagName('FOOTER');
+
+
+/** @type {!goog.dom.TagName<!HTMLFormElement>} */
+goog.dom.TagName.FORM = new goog.dom.TagName('FORM');
+
+
+/** @type {!goog.dom.TagName<!HTMLFrameElement>} */
+goog.dom.TagName.FRAME = new goog.dom.TagName('FRAME');
+
+
+/** @type {!goog.dom.TagName<!HTMLFrameSetElement>} */
+goog.dom.TagName.FRAMESET = new goog.dom.TagName('FRAMESET');
+
+
+/** @type {!goog.dom.TagName<!HTMLHeadingElement>} */
+goog.dom.TagName.H1 = new goog.dom.TagName('H1');
+
+
+/** @type {!goog.dom.TagName<!HTMLHeadingElement>} */
+goog.dom.TagName.H2 = new goog.dom.TagName('H2');
+
+
+/** @type {!goog.dom.TagName<!HTMLHeadingElement>} */
+goog.dom.TagName.H3 = new goog.dom.TagName('H3');
+
+
+/** @type {!goog.dom.TagName<!HTMLHeadingElement>} */
+goog.dom.TagName.H4 = new goog.dom.TagName('H4');
+
+
+/** @type {!goog.dom.TagName<!HTMLHeadingElement>} */
+goog.dom.TagName.H5 = new goog.dom.TagName('H5');
+
+
+/** @type {!goog.dom.TagName<!HTMLHeadingElement>} */
+goog.dom.TagName.H6 = new goog.dom.TagName('H6');
+
+
+/** @type {!goog.dom.TagName<!HTMLHeadElement>} */
+goog.dom.TagName.HEAD = new goog.dom.TagName('HEAD');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.HEADER = new goog.dom.TagName('HEADER');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.HGROUP = new goog.dom.TagName('HGROUP');
+
+
+/** @type {!goog.dom.TagName<!HTMLHRElement>} */
+goog.dom.TagName.HR = new goog.dom.TagName('HR');
+
+
+/** @type {!goog.dom.TagName<!HTMLHtmlElement>} */
+goog.dom.TagName.HTML = new goog.dom.TagName('HTML');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.I = new goog.dom.TagName('I');
+
+
+/** @type {!goog.dom.TagName<!HTMLIFrameElement>} */
+goog.dom.TagName.IFRAME = new goog.dom.TagName('IFRAME');
+
+
+/** @type {!goog.dom.TagName<!HTMLImageElement>} */
+goog.dom.TagName.IMG = new goog.dom.TagName('IMG');
+
+
+/** @type {!goog.dom.TagName<!HTMLInputElement>} */
+goog.dom.TagName.INPUT = new goog.dom.TagName('INPUT');
+
+
+/** @type {!goog.dom.TagName<!HTMLModElement>} */
+goog.dom.TagName.INS = new goog.dom.TagName('INS');
+
+
+/** @type {!goog.dom.TagName<!HTMLIsIndexElement>} */
+goog.dom.TagName.ISINDEX = new goog.dom.TagName('ISINDEX');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.KBD = new goog.dom.TagName('KBD');
+
+
+// HTMLKeygenElement is deprecated.
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.KEYGEN = new goog.dom.TagName('KEYGEN');
+
+
+/** @type {!goog.dom.TagName<!HTMLLabelElement>} */
+goog.dom.TagName.LABEL = new goog.dom.TagName('LABEL');
+
+
+/** @type {!goog.dom.TagName<!HTMLLegendElement>} */
+goog.dom.TagName.LEGEND = new goog.dom.TagName('LEGEND');
+
+
+/** @type {!goog.dom.TagName<!HTMLLIElement>} */
+goog.dom.TagName.LI = new goog.dom.TagName('LI');
+
+
+/** @type {!goog.dom.TagName<!HTMLLinkElement>} */
+goog.dom.TagName.LINK = new goog.dom.TagName('LINK');
+
+
+/** @type {!goog.dom.TagName<!HTMLMapElement>} */
+goog.dom.TagName.MAP = new goog.dom.TagName('MAP');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.MARK = new goog.dom.TagName('MARK');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.MATH = new goog.dom.TagName('MATH');
+
+
+/** @type {!goog.dom.TagName<!HTMLMenuElement>} */
+goog.dom.TagName.MENU = new goog.dom.TagName('MENU');
+
+
+/** @type {!goog.dom.TagName<!HTMLMetaElement>} */
+goog.dom.TagName.META = new goog.dom.TagName('META');
+
+
+/** @type {!goog.dom.TagName<!HTMLMeterElement>} */
+goog.dom.TagName.METER = new goog.dom.TagName('METER');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.NAV = new goog.dom.TagName('NAV');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.NOFRAMES = new goog.dom.TagName('NOFRAMES');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.NOSCRIPT = new goog.dom.TagName('NOSCRIPT');
+
+
+/** @type {!goog.dom.TagName<!HTMLObjectElement>} */
+goog.dom.TagName.OBJECT = new goog.dom.TagName('OBJECT');
+
+
+/** @type {!goog.dom.TagName<!HTMLOListElement>} */
+goog.dom.TagName.OL = new goog.dom.TagName('OL');
+
+
+/** @type {!goog.dom.TagName<!HTMLOptGroupElement>} */
+goog.dom.TagName.OPTGROUP = new goog.dom.TagName('OPTGROUP');
+
+
+/** @type {!goog.dom.TagName<!HTMLOptionElement>} */
+goog.dom.TagName.OPTION = new goog.dom.TagName('OPTION');
+
+
+/** @type {!goog.dom.TagName<!HTMLOutputElement>} */
+goog.dom.TagName.OUTPUT = new goog.dom.TagName('OUTPUT');
+
+
+/** @type {!goog.dom.TagName<!HTMLParagraphElement>} */
+goog.dom.TagName.P = new goog.dom.TagName('P');
+
+
+/** @type {!goog.dom.TagName<!HTMLParamElement>} */
+goog.dom.TagName.PARAM = new goog.dom.TagName('PARAM');
+
+
+/** @type {!goog.dom.TagName<!HTMLPreElement>} */
+goog.dom.TagName.PRE = new goog.dom.TagName('PRE');
+
+
+/** @type {!goog.dom.TagName<!HTMLProgressElement>} */
+goog.dom.TagName.PROGRESS = new goog.dom.TagName('PROGRESS');
+
+
+/** @type {!goog.dom.TagName<!HTMLQuoteElement>} */
+goog.dom.TagName.Q = new goog.dom.TagName('Q');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.RP = new goog.dom.TagName('RP');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.RT = new goog.dom.TagName('RT');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.RUBY = new goog.dom.TagName('RUBY');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.S = new goog.dom.TagName('S');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.SAMP = new goog.dom.TagName('SAMP');
+
+
+/** @type {!goog.dom.TagName<!HTMLScriptElement>} */
+goog.dom.TagName.SCRIPT = new goog.dom.TagName('SCRIPT');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.SECTION = new goog.dom.TagName('SECTION');
+
+
+/** @type {!goog.dom.TagName<!HTMLSelectElement>} */
+goog.dom.TagName.SELECT = new goog.dom.TagName('SELECT');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.SMALL = new goog.dom.TagName('SMALL');
+
+
+/** @type {!goog.dom.TagName<!HTMLSourceElement>} */
+goog.dom.TagName.SOURCE = new goog.dom.TagName('SOURCE');
+
+
+/** @type {!goog.dom.TagName<!HTMLSpanElement>} */
+goog.dom.TagName.SPAN = new goog.dom.TagName('SPAN');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.STRIKE = new goog.dom.TagName('STRIKE');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.STRONG = new goog.dom.TagName('STRONG');
+
+
+/** @type {!goog.dom.TagName<!HTMLStyleElement>} */
+goog.dom.TagName.STYLE = new goog.dom.TagName('STYLE');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.SUB = new goog.dom.TagName('SUB');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.SUMMARY = new goog.dom.TagName('SUMMARY');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.SUP = new goog.dom.TagName('SUP');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.SVG = new goog.dom.TagName('SVG');
+
+
+/** @type {!goog.dom.TagName<!HTMLTableElement>} */
+goog.dom.TagName.TABLE = new goog.dom.TagName('TABLE');
+
+
+/** @type {!goog.dom.TagName<!HTMLTableSectionElement>} */
+goog.dom.TagName.TBODY = new goog.dom.TagName('TBODY');
+
+
+/** @type {!goog.dom.TagName<!HTMLTableCellElement>} */
+goog.dom.TagName.TD = new goog.dom.TagName('TD');
+
+
+/** @type {!goog.dom.TagName<!HTMLTemplateElement>} */
+goog.dom.TagName.TEMPLATE = new goog.dom.TagName('TEMPLATE');
+
+
+/** @type {!goog.dom.TagName<!HTMLTextAreaElement>} */
+goog.dom.TagName.TEXTAREA = new goog.dom.TagName('TEXTAREA');
+
+
+/** @type {!goog.dom.TagName<!HTMLTableSectionElement>} */
+goog.dom.TagName.TFOOT = new goog.dom.TagName('TFOOT');
+
+
+/** @type {!goog.dom.TagName<!HTMLTableCellElement>} */
+goog.dom.TagName.TH = new goog.dom.TagName('TH');
+
+
+/** @type {!goog.dom.TagName<!HTMLTableSectionElement>} */
+goog.dom.TagName.THEAD = new goog.dom.TagName('THEAD');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.TIME = new goog.dom.TagName('TIME');
+
+
+/** @type {!goog.dom.TagName<!HTMLTitleElement>} */
+goog.dom.TagName.TITLE = new goog.dom.TagName('TITLE');
+
+
+/** @type {!goog.dom.TagName<!HTMLTableRowElement>} */
+goog.dom.TagName.TR = new goog.dom.TagName('TR');
+
+
+/** @type {!goog.dom.TagName<!HTMLTrackElement>} */
+goog.dom.TagName.TRACK = new goog.dom.TagName('TRACK');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.TT = new goog.dom.TagName('TT');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.U = new goog.dom.TagName('U');
+
+
+/** @type {!goog.dom.TagName<!HTMLUListElement>} */
+goog.dom.TagName.UL = new goog.dom.TagName('UL');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.VAR = new goog.dom.TagName('VAR');
+
+
+/** @type {!goog.dom.TagName<!HTMLVideoElement>} */
+goog.dom.TagName.VIDEO = new goog.dom.TagName('VIDEO');
+
+
+/** @type {!goog.dom.TagName<!goog.dom.HtmlElement>} */
+goog.dom.TagName.WBR = new goog.dom.TagName('WBR');
diff --git a/third_party/ink/closure/dom/tags.js b/third_party/ink/closure/dom/tags.js
new file mode 100644
index 0000000..7c12938
--- /dev/null
+++ b/third_party/ink/closure/dom/tags.js
@@ -0,0 +1,41 @@
+// Copyright 2014 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 Utilities for HTML element tag names.
+ */
+goog.provide('goog.dom.tags');
+
+goog.require('goog.object');
+
+
+/**
+ * The void elements specified by
+ * http://www.w3.org/TR/html-markup/syntax.html#void-elements.
+ * @const @private {!Object<string, boolean>}
+ */
+goog.dom.tags.VOID_TAGS_ = goog.object.createSet(
+    'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input',
+    'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr');
+
+
+/**
+ * Checks whether the tag is void (with no contents allowed and no legal end
+ * tag), for example 'br'.
+ * @param {string} tagName The tag name in lower case.
+ * @return {boolean}
+ */
+goog.dom.tags.isVoidTag = function(tagName) {
+  return goog.dom.tags.VOID_TAGS_[tagName] === true;
+};
diff --git a/third_party/ink/closure/dom/vendor.js b/third_party/ink/closure/dom/vendor.js
new file mode 100644
index 0000000..28b173d
--- /dev/null
+++ b/third_party/ink/closure/dom/vendor.js
@@ -0,0 +1,97 @@
+// Copyright 2012 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 Vendor prefix getters.
+ */
+
+goog.provide('goog.dom.vendor');
+
+goog.require('goog.string');
+goog.require('goog.userAgent');
+
+
+/**
+ * Returns the JS vendor prefix used in CSS properties. Different vendors
+ * use different methods of changing the case of the property names.
+ *
+ * @return {?string} The JS vendor prefix or null if there is none.
+ */
+goog.dom.vendor.getVendorJsPrefix = function() {
+  if (goog.userAgent.WEBKIT) {
+    return 'Webkit';
+  } else if (goog.userAgent.GECKO) {
+    return 'Moz';
+  } else if (goog.userAgent.IE) {
+    return 'ms';
+  } else if (goog.userAgent.OPERA) {
+    return 'O';
+  }
+
+  return null;
+};
+
+
+/**
+ * Returns the vendor prefix used in CSS properties.
+ *
+ * @return {?string} The vendor prefix or null if there is none.
+ */
+goog.dom.vendor.getVendorPrefix = function() {
+  if (goog.userAgent.WEBKIT) {
+    return '-webkit';
+  } else if (goog.userAgent.GECKO) {
+    return '-moz';
+  } else if (goog.userAgent.IE) {
+    return '-ms';
+  } else if (goog.userAgent.OPERA) {
+    return '-o';
+  }
+
+  return null;
+};
+
+
+/**
+ * @param {string} propertyName A property name.
+ * @param {!Object=} opt_object If provided, we verify if the property exists in
+ *     the object.
+ * @return {?string} A vendor prefixed property name, or null if it does not
+ *     exist.
+ */
+goog.dom.vendor.getPrefixedPropertyName = function(propertyName, opt_object) {
+  // We first check for a non-prefixed property, if available.
+  if (opt_object && propertyName in opt_object) {
+    return propertyName;
+  }
+  var prefix = goog.dom.vendor.getVendorJsPrefix();
+  if (prefix) {
+    prefix = prefix.toLowerCase();
+    var prefixedPropertyName = prefix + goog.string.toTitleCase(propertyName);
+    return (!goog.isDef(opt_object) || prefixedPropertyName in opt_object) ?
+        prefixedPropertyName :
+        null;
+  }
+  return null;
+};
+
+
+/**
+ * @param {string} eventType An event type.
+ * @return {string} A lower-cased vendor prefixed event type.
+ */
+goog.dom.vendor.getPrefixedEventType = function(eventType) {
+  var prefix = goog.dom.vendor.getVendorJsPrefix() || '';
+  return (prefix + eventType).toLowerCase();
+};
diff --git a/third_party/ink/closure/events/browserevent.js b/third_party/ink/closure/events/browserevent.js
new file mode 100644
index 0000000..3c557ca
--- /dev/null
+++ b/third_party/ink/closure/events/browserevent.js
@@ -0,0 +1,470 @@
+// Copyright 2005 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 A patched, standardized event object for browser events.
+ *
+ * <pre>
+ * The patched event object contains the following members:
+ * - type           {string}    Event type, e.g. 'click'
+ * - target         {Object}    The element that actually triggered the event
+ * - currentTarget  {Object}    The element the listener is attached to
+ * - relatedTarget  {Object}    For mouseover and mouseout, the previous object
+ * - offsetX        {number}    X-coordinate relative to target
+ * - offsetY        {number}    Y-coordinate relative to target
+ * - clientX        {number}    X-coordinate relative to viewport
+ * - clientY        {number}    Y-coordinate relative to viewport
+ * - screenX        {number}    X-coordinate relative to the edge of the screen
+ * - screenY        {number}    Y-coordinate relative to the edge of the screen
+ * - button         {number}    Mouse button. Use isButton() to test.
+ * - keyCode        {number}    Key-code
+ * - ctrlKey        {boolean}   Was ctrl key depressed
+ * - altKey         {boolean}   Was alt key depressed
+ * - shiftKey       {boolean}   Was shift key depressed
+ * - metaKey        {boolean}   Was meta key depressed
+ * - pointerId      {number}    Pointer ID
+ * - pointerType    {string}    Pointer type, e.g. 'mouse', 'pen', or 'touch'
+ * - defaultPrevented {boolean} Whether the default action has been prevented
+ * - state          {Object}    History state object
+ *
+ * NOTE: The keyCode member contains the raw browser keyCode. For normalized
+ * key and character code use {@link goog.events.KeyHandler}.
+ * </pre>
+ *
+ * @author pupius@google.com (Daniel Pupius)
+ * @author arv@google.com (Erik Arvidsson)
+ */
+
+goog.provide('goog.events.BrowserEvent');
+goog.provide('goog.events.BrowserEvent.MouseButton');
+goog.provide('goog.events.BrowserEvent.PointerType');
+
+goog.require('goog.debug');
+goog.require('goog.events.BrowserFeature');
+goog.require('goog.events.Event');
+goog.require('goog.events.EventType');
+goog.require('goog.reflect');
+goog.require('goog.userAgent');
+
+
+
+/**
+ * Accepts a browser event object and creates a patched, cross browser event
+ * object.
+ * The content of this object will not be initialized if no event object is
+ * provided. If this is the case, init() needs to be invoked separately.
+ * @param {Event=} opt_e Browser event object.
+ * @param {EventTarget=} opt_currentTarget Current target for event.
+ * @constructor
+ * @extends {goog.events.Event}
+ */
+goog.events.BrowserEvent = function(opt_e, opt_currentTarget) {
+  goog.events.BrowserEvent.base(this, 'constructor', opt_e ? opt_e.type : '');
+
+  /**
+   * Target that fired the event.
+   * @override
+   * @type {Node}
+   */
+  this.target = null;
+
+  /**
+   * Node that had the listener attached.
+   * @override
+   * @type {Node|undefined}
+   */
+  this.currentTarget = null;
+
+  /**
+   * For mouseover and mouseout events, the related object for the event.
+   * @type {Node}
+   */
+  this.relatedTarget = null;
+
+  /**
+   * X-coordinate relative to target.
+   * @type {number}
+   */
+  this.offsetX = 0;
+
+  /**
+   * Y-coordinate relative to target.
+   * @type {number}
+   */
+  this.offsetY = 0;
+
+  /**
+   * X-coordinate relative to the window.
+   * @type {number}
+   */
+  this.clientX = 0;
+
+  /**
+   * Y-coordinate relative to the window.
+   * @type {number}
+   */
+  this.clientY = 0;
+
+  /**
+   * X-coordinate relative to the monitor.
+   * @type {number}
+   */
+  this.screenX = 0;
+
+  /**
+   * Y-coordinate relative to the monitor.
+   * @type {number}
+   */
+  this.screenY = 0;
+
+  /**
+   * Which mouse button was pressed.
+   * @type {number}
+   */
+  this.button = 0;
+
+  /**
+   * Key of key press.
+   * @type {string}
+   */
+  this.key = '';
+
+  /**
+   * Keycode of key press.
+   * @type {number}
+   */
+  this.keyCode = 0;
+
+  /**
+   * Keycode of key press.
+   * @type {number}
+   */
+  this.charCode = 0;
+
+  /**
+   * Whether control was pressed at time of event.
+   * @type {boolean}
+   */
+  this.ctrlKey = false;
+
+  /**
+   * Whether alt was pressed at time of event.
+   * @type {boolean}
+   */
+  this.altKey = false;
+
+  /**
+   * Whether shift was pressed at time of event.
+   * @type {boolean}
+   */
+  this.shiftKey = false;
+
+  /**
+   * Whether the meta key was pressed at time of event.
+   * @type {boolean}
+   */
+  this.metaKey = false;
+
+  /**
+   * History state object, only set for PopState events where it's a copy of the
+   * state object provided to pushState or replaceState.
+   * @type {Object}
+   */
+  this.state = null;
+
+  /**
+   * Whether the default platform modifier key was pressed at time of event.
+   * (This is control for all platforms except Mac, where it's Meta.)
+   * @type {boolean}
+   */
+  this.platformModifierKey = false;
+
+  /**
+   * @type {number}
+   */
+  this.pointerId = 0;
+
+  /**
+   * @type {string}
+   */
+  this.pointerType = '';
+
+  /**
+   * The browser event object.
+   * @private {Event}
+   */
+  this.event_ = null;
+
+  if (opt_e) {
+    this.init(opt_e, opt_currentTarget);
+  }
+};
+goog.inherits(goog.events.BrowserEvent, goog.events.Event);
+
+
+/**
+ * Normalized button constants for the mouse.
+ * @enum {number}
+ */
+goog.events.BrowserEvent.MouseButton = {
+  LEFT: 0,
+  MIDDLE: 1,
+  RIGHT: 2
+};
+
+
+/**
+ * Normalized pointer type constants for pointer events.
+ * @enum {string}
+ */
+goog.events.BrowserEvent.PointerType = {
+  MOUSE: 'mouse',
+  PEN: 'pen',
+  TOUCH: 'touch'
+};
+
+
+/**
+ * Static data for mapping mouse buttons.
+ * @type {!Array<number>}
+ * @deprecated Use {@code goog.events.BrowserEvent.IE_BUTTON_MAP} instead.
+ */
+goog.events.BrowserEvent.IEButtonMap = goog.debug.freeze([
+  1,  // LEFT
+  4,  // MIDDLE
+  2   // RIGHT
+]);
+
+
+/**
+ * Static data for mapping mouse buttons.
+ * @const {!Array<number>}
+ */
+goog.events.BrowserEvent.IE_BUTTON_MAP = goog.events.BrowserEvent.IEButtonMap;
+
+
+/**
+ * Static data for mapping MSPointerEvent types to PointerEvent types.
+ * @const {!Object<number, goog.events.BrowserEvent.PointerType>}
+ */
+goog.events.BrowserEvent.IE_POINTER_TYPE_MAP = goog.debug.freeze({
+  2: goog.events.BrowserEvent.PointerType.TOUCH,
+  3: goog.events.BrowserEvent.PointerType.PEN,
+  4: goog.events.BrowserEvent.PointerType.MOUSE
+});
+
+
+/**
+ * Accepts a browser event object and creates a patched, cross browser event
+ * object.
+ * @param {Event} e Browser event object.
+ * @param {EventTarget=} opt_currentTarget Current target for event.
+ */
+goog.events.BrowserEvent.prototype.init = function(e, opt_currentTarget) {
+  var type = this.type = e.type;
+
+  /**
+   * On touch devices use the first "changed touch" as the relevant touch.
+   * @type {Touch}
+   */
+  var relevantTouch = e.changedTouches ? e.changedTouches[0] : null;
+
+  // TODO(nicksantos): Change this.target to type EventTarget.
+  this.target = /** @type {Node} */ (e.target) || e.srcElement;
+
+  // TODO(nicksantos): Change this.currentTarget to type EventTarget.
+  this.currentTarget = /** @type {Node} */ (opt_currentTarget);
+
+  var relatedTarget = /** @type {Node} */ (e.relatedTarget);
+  if (relatedTarget) {
+    // There's a bug in FireFox where sometimes, relatedTarget will be a
+    // chrome element, and accessing any property of it will get a permission
+    // denied exception. See:
+    // https://bugzilla.mozilla.org/show_bug.cgi?id=497780
+    if (goog.userAgent.GECKO) {
+      if (!goog.reflect.canAccessProperty(relatedTarget, 'nodeName')) {
+        relatedTarget = null;
+      }
+    }
+  } else if (type == goog.events.EventType.MOUSEOVER) {
+    relatedTarget = e.fromElement;
+  } else if (type == goog.events.EventType.MOUSEOUT) {
+    relatedTarget = e.toElement;
+  }
+
+  this.relatedTarget = relatedTarget;
+
+  if (!goog.isNull(relevantTouch)) {
+    this.clientX = relevantTouch.clientX !== undefined ? relevantTouch.clientX :
+                                                         relevantTouch.pageX;
+    this.clientY = relevantTouch.clientY !== undefined ? relevantTouch.clientY :
+                                                         relevantTouch.pageY;
+    this.screenX = relevantTouch.screenX || 0;
+    this.screenY = relevantTouch.screenY || 0;
+  } else {
+    // Webkit emits a lame warning whenever layerX/layerY is accessed.
+    // http://code.google.com/p/chromium/issues/detail?id=101733
+    this.offsetX = (goog.userAgent.WEBKIT || e.offsetX !== undefined) ?
+        e.offsetX :
+        e.layerX;
+    this.offsetY = (goog.userAgent.WEBKIT || e.offsetY !== undefined) ?
+        e.offsetY :
+        e.layerY;
+    this.clientX = e.clientX !== undefined ? e.clientX : e.pageX;
+    this.clientY = e.clientY !== undefined ? e.clientY : e.pageY;
+    this.screenX = e.screenX || 0;
+    this.screenY = e.screenY || 0;
+  }
+
+  this.button = e.button;
+
+  this.keyCode = e.keyCode || 0;
+  this.key = e.key || '';
+  this.charCode = e.charCode || (type == 'keypress' ? e.keyCode : 0);
+  this.ctrlKey = e.ctrlKey;
+  this.altKey = e.altKey;
+  this.shiftKey = e.shiftKey;
+  this.metaKey = e.metaKey;
+  this.platformModifierKey = goog.userAgent.MAC ? e.metaKey : e.ctrlKey;
+  this.pointerId = e.pointerId || 0;
+  this.pointerType = goog.events.BrowserEvent.getPointerType_(e);
+  this.state = e.state;
+  this.event_ = e;
+  if (e.defaultPrevented) {
+    this.preventDefault();
+  }
+};
+
+
+/**
+ * Tests to see which button was pressed during the event. This is really only
+ * useful in IE and Gecko browsers. And in IE, it's only useful for
+ * mousedown/mouseup events, because click only fires for the left mouse button.
+ *
+ * Safari 2 only reports the left button being clicked, and uses the value '1'
+ * instead of 0. Opera only reports a mousedown event for the middle button, and
+ * no mouse events for the right button. Opera has default behavior for left and
+ * middle click that can only be overridden via a configuration setting.
+ *
+ * There's a nice table of this mess at http://www.unixpapa.com/js/mouse.html.
+ *
+ * @param {goog.events.BrowserEvent.MouseButton} button The button
+ *     to test for.
+ * @return {boolean} True if button was pressed.
+ */
+goog.events.BrowserEvent.prototype.isButton = function(button) {
+  if (!goog.events.BrowserFeature.HAS_W3C_BUTTON) {
+    if (this.type == 'click') {
+      return button == goog.events.BrowserEvent.MouseButton.LEFT;
+    } else {
+      return !!(
+          this.event_.button & goog.events.BrowserEvent.IE_BUTTON_MAP[button]);
+    }
+  } else {
+    return this.event_.button == button;
+  }
+};
+
+
+/**
+ * Whether this has an "action"-producing mouse button.
+ *
+ * By definition, this includes left-click on windows/linux, and left-click
+ * without the ctrl key on Macs.
+ *
+ * @return {boolean} The result.
+ */
+goog.events.BrowserEvent.prototype.isMouseActionButton = function() {
+  // Webkit does not ctrl+click to be a right-click, so we
+  // normalize it to behave like Gecko and Opera.
+  return this.isButton(goog.events.BrowserEvent.MouseButton.LEFT) &&
+      !(goog.userAgent.WEBKIT && goog.userAgent.MAC && this.ctrlKey);
+};
+
+
+/**
+ * @override
+ */
+goog.events.BrowserEvent.prototype.stopPropagation = function() {
+  goog.events.BrowserEvent.superClass_.stopPropagation.call(this);
+  if (this.event_.stopPropagation) {
+    this.event_.stopPropagation();
+  } else {
+    this.event_.cancelBubble = true;
+  }
+};
+
+
+/**
+ * @override
+ */
+goog.events.BrowserEvent.prototype.preventDefault = function() {
+  goog.events.BrowserEvent.superClass_.preventDefault.call(this);
+  var be = this.event_;
+  if (!be.preventDefault) {
+    be.returnValue = false;
+    if (goog.events.BrowserFeature.SET_KEY_CODE_TO_PREVENT_DEFAULT) {
+
+      try {
+        // Most keys can be prevented using returnValue. Some special keys
+        // require setting the keyCode to -1 as well:
+        //
+        // In IE7:
+        // F3, F5, F10, F11, Ctrl+P, Crtl+O, Ctrl+F (these are taken from IE6)
+        //
+        // In IE8:
+        // Ctrl+P, Crtl+O, Ctrl+F (F1-F12 cannot be stopped through the event)
+        //
+        // We therefore do this for all function keys as well as when Ctrl key
+        // is pressed.
+        var VK_F1 = 112;
+        var VK_F12 = 123;
+        if (be.ctrlKey || be.keyCode >= VK_F1 && be.keyCode <= VK_F12) {
+          be.keyCode = -1;
+        }
+      } catch (ex) {
+        // IE throws an 'access denied' exception when trying to change
+        // keyCode in some situations (e.g. srcElement is input[type=file],
+        // or srcElement is an anchor tag rewritten by parent's innerHTML).
+        // Do nothing in this case.
+      }
+    }
+  } else {
+    be.preventDefault();
+  }
+};
+
+
+/**
+ * @return {Event} The underlying browser event object.
+ */
+goog.events.BrowserEvent.prototype.getBrowserEvent = function() {
+  return this.event_;
+};
+
+
+/**
+ * Extracts the pointer type from the given event.
+ * @param {!Event} e
+ * @return {string} The pointer type, e.g. 'mouse', 'pen', or 'touch'.
+ * @private
+ */
+goog.events.BrowserEvent.getPointerType_ = function(e) {
+  if (goog.isString(e.pointerType)) {
+    return e.pointerType;
+  }
+  // IE10 uses integer codes for pointer type.
+  // https://msdn.microsoft.com/en-us/library/hh772359(v=vs.85).aspx
+  return goog.events.BrowserEvent.IE_POINTER_TYPE_MAP[e.pointerType] || '';
+};
diff --git a/third_party/ink/closure/events/browserfeature.js b/third_party/ink/closure/events/browserfeature.js
new file mode 100644
index 0000000..10ef20d
--- /dev/null
+++ b/third_party/ink/closure/events/browserfeature.js
@@ -0,0 +1,140 @@
+// Copyright 2010 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 Browser capability checks for the events package.
+ *
+ * @author zhyder@google.com (Zohair Hyder)
+ */
+
+
+goog.provide('goog.events.BrowserFeature');
+
+goog.require('goog.userAgent');
+goog.scope(function() {
+
+
+
+/**
+ * Enum of browser capabilities.
+ * @enum {boolean}
+ */
+goog.events.BrowserFeature = {
+  /**
+   * Whether the button attribute of the event is W3C compliant.  False in
+   * Internet Explorer prior to version 9; document-version dependent.
+   */
+  HAS_W3C_BUTTON:
+      !goog.userAgent.IE || goog.userAgent.isDocumentModeOrHigher(9),
+
+  /**
+   * Whether the browser supports full W3C event model.
+   */
+  HAS_W3C_EVENT_SUPPORT:
+      !goog.userAgent.IE || goog.userAgent.isDocumentModeOrHigher(9),
+
+  /**
+   * To prevent default in IE7-8 for certain keydown events we need set the
+   * keyCode to -1.
+   */
+  SET_KEY_CODE_TO_PREVENT_DEFAULT:
+      goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9'),
+
+  /**
+   * Whether the {@code navigator.onLine} property is supported.
+   */
+  HAS_NAVIGATOR_ONLINE_PROPERTY:
+      !goog.userAgent.WEBKIT || goog.userAgent.isVersionOrHigher('528'),
+
+  /**
+   * Whether HTML5 network online/offline events are supported.
+   */
+  HAS_HTML5_NETWORK_EVENT_SUPPORT:
+      goog.userAgent.GECKO && goog.userAgent.isVersionOrHigher('1.9b') ||
+      goog.userAgent.IE && goog.userAgent.isVersionOrHigher('8') ||
+      goog.userAgent.OPERA && goog.userAgent.isVersionOrHigher('9.5') ||
+      goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher('528'),
+
+  /**
+   * Whether HTML5 network events fire on document.body, or otherwise the
+   * window.
+   */
+  HTML5_NETWORK_EVENTS_FIRE_ON_BODY:
+      goog.userAgent.GECKO && !goog.userAgent.isVersionOrHigher('8') ||
+      goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9'),
+
+  /**
+   * Whether touch is enabled in the browser.
+   */
+  TOUCH_ENABLED:
+      ('ontouchstart' in goog.global ||
+       !!(goog.global['document'] && document.documentElement &&
+          'ontouchstart' in document.documentElement) ||
+       // IE10 uses non-standard touch events, so it has a different check.
+       !!(goog.global['navigator'] &&
+          (goog.global['navigator']['maxTouchPoints'] ||
+           goog.global['navigator']['msMaxTouchPoints']))),
+
+  /**
+   * Whether addEventListener supports W3C standard pointer events.
+   * http://www.w3.org/TR/pointerevents/
+   */
+  POINTER_EVENTS: ('PointerEvent' in goog.global),
+
+  /**
+   * Whether addEventListener supports MSPointer events (only used in IE10).
+   * http://msdn.microsoft.com/en-us/library/ie/hh772103(v=vs.85).aspx
+   * http://msdn.microsoft.com/library/hh673557(v=vs.85).aspx
+   */
+  MSPOINTER_EVENTS:
+      ('MSPointerEvent' in goog.global &&
+       !!(goog.global['navigator'] &&
+          goog.global['navigator']['msPointerEnabled'])),
+
+  /**
+   * Whether addEventListener supports {passive: true}.
+   * https://developers.google.com/web/updates/2016/06/passive-event-listeners
+   */
+  PASSIVE_EVENTS: purify(function() {
+    // If we're in a web worker or other custom environment, we can't tell.
+    if (!goog.global.addEventListener || !Object.defineProperty) {  // IE 8
+      return false;
+    }
+
+    var passive = false;
+    var options = Object.defineProperty({}, 'passive', {
+      get: function() {
+        passive = true;
+      }
+    });
+    goog.global.addEventListener('test', goog.nullFunction, options);
+    goog.global.removeEventListener('test', goog.nullFunction, options);
+
+    return passive;
+  })
+};
+
+
+/**
+ * Tricks Closure Compiler into believing that a function is pure.  The compiler
+ * assumes that any `valueOf` function is pure, without analyzing its contents.
+ *
+ * @param {function(): T} fn
+ * @return {T}
+ * @template T
+ */
+function purify(fn) {
+  return ({valueOf: fn}).valueOf();
+}
+});  // goog.scope
diff --git a/third_party/ink/closure/events/event.js b/third_party/ink/closure/events/event.js
new file mode 100644
index 0000000..22efba9
--- /dev/null
+++ b/third_party/ink/closure/events/event.js
@@ -0,0 +1,144 @@
+// Copyright 2005 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 A base class for event objects.
+ *
+ * @author pupius@google.com (Daniel Pupius)
+ */
+
+
+goog.provide('goog.events.Event');
+goog.provide('goog.events.EventLike');
+
+/**
+ * goog.events.Event no longer depends on goog.Disposable. Keep requiring
+ * goog.Disposable here to not break projects which assume this dependency.
+ * @suppress {extraRequire}
+ */
+goog.require('goog.Disposable');
+goog.require('goog.events.EventId');
+
+
+/**
+ * A typedef for event like objects that are dispatchable via the
+ * goog.events.dispatchEvent function. strings are treated as the type for a
+ * goog.events.Event. Objects are treated as an extension of a new
+ * goog.events.Event with the type property of the object being used as the type
+ * of the Event.
+ * @typedef {string|Object|goog.events.Event|goog.events.EventId}
+ */
+goog.events.EventLike;
+
+
+
+/**
+ * A base class for event objects, so that they can support preventDefault and
+ * stopPropagation.
+ *
+ * @suppress {underscore} Several properties on this class are technically
+ *     public, but referencing these properties outside this package is strongly
+ *     discouraged.
+ *
+ * @param {string|!goog.events.EventId} type Event Type.
+ * @param {Object=} opt_target Reference to the object that is the target of
+ *     this event. It has to implement the {@code EventTarget} interface
+ *     declared at {@link http://developer.mozilla.org/en/DOM/EventTarget}.
+ * @constructor
+ */
+goog.events.Event = function(type, opt_target) {
+  /**
+   * Event type.
+   * @type {string}
+   */
+  this.type = type instanceof goog.events.EventId ? String(type) : type;
+
+  /**
+   * TODO(tbreisacher): The type should probably be
+   * EventTarget|goog.events.EventTarget.
+   *
+   * Target of the event.
+   * @type {Object|undefined}
+   */
+  this.target = opt_target;
+
+  /**
+   * Object that had the listener attached.
+   * @type {Object|undefined}
+   */
+  this.currentTarget = this.target;
+
+  /**
+   * Whether to cancel the event in internal capture/bubble processing for IE.
+   * @type {boolean}
+   * @public
+   */
+  this.propagationStopped_ = false;
+
+  /**
+   * Whether the default action has been prevented.
+   * This is a property to match the W3C specification at
+   * {@link http://www.w3.org/TR/DOM-Level-3-Events/
+   * #events-event-type-defaultPrevented}.
+   * Must be treated as read-only outside the class.
+   * @type {boolean}
+   */
+  this.defaultPrevented = false;
+
+  /**
+   * Return value for in internal capture/bubble processing for IE.
+   * @type {boolean}
+   * @public
+   */
+  this.returnValue_ = true;
+};
+
+
+/**
+ * Stops event propagation.
+ */
+goog.events.Event.prototype.stopPropagation = function() {
+  this.propagationStopped_ = true;
+};
+
+
+/**
+ * Prevents the default action, for example a link redirecting to a url.
+ */
+goog.events.Event.prototype.preventDefault = function() {
+  this.defaultPrevented = true;
+  this.returnValue_ = false;
+};
+
+
+/**
+ * Stops the propagation of the event. It is equivalent to
+ * {@code e.stopPropagation()}, but can be used as the callback argument of
+ * {@link goog.events.listen} without declaring another function.
+ * @param {!goog.events.Event} e An event.
+ */
+goog.events.Event.stopPropagation = function(e) {
+  e.stopPropagation();
+};
+
+
+/**
+ * Prevents the default action. It is equivalent to
+ * {@code e.preventDefault()}, but can be used as the callback argument of
+ * {@link goog.events.listen} without declaring another function.
+ * @param {!goog.events.Event} e An event.
+ */
+goog.events.Event.preventDefault = function(e) {
+  e.preventDefault();
+};
diff --git a/third_party/ink/closure/events/eventhandler.js b/third_party/ink/closure/events/eventhandler.js
new file mode 100644
index 0000000..6a887f9
--- /dev/null
+++ b/third_party/ink/closure/events/eventhandler.js
@@ -0,0 +1,479 @@
+// Copyright 2005 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 Class to create objects which want to handle multiple events
+ * and have their listeners easily cleaned up via a dispose method.
+ *
+ * Example:
+ * <pre>
+ * function Something() {
+ *   Something.base(this);
+ *
+ *   ... set up object ...
+ *
+ *   // Add event listeners
+ *   this.listen(this.starEl, goog.events.EventType.CLICK, this.handleStar);
+ *   this.listen(this.headerEl, goog.events.EventType.CLICK, this.expand);
+ *   this.listen(this.collapseEl, goog.events.EventType.CLICK, this.collapse);
+ *   this.listen(this.infoEl, goog.events.EventType.MOUSEOVER, this.showHover);
+ *   this.listen(this.infoEl, goog.events.EventType.MOUSEOUT, this.hideHover);
+ * }
+ * goog.inherits(Something, goog.events.EventHandler);
+ *
+ * Something.prototype.disposeInternal = function() {
+ *   Something.base(this, 'disposeInternal');
+ *   goog.dom.removeNode(this.container);
+ * };
+ *
+ *
+ * // Then elsewhere:
+ *
+ * var activeSomething = null;
+ * function openSomething() {
+ *   activeSomething = new Something();
+ * }
+ *
+ * function closeSomething() {
+ *   if (activeSomething) {
+ *     activeSomething.dispose();  // Remove event listeners
+ *     activeSomething = null;
+ *   }
+ * }
+ * </pre>
+ *
+ * @author pupius@google.com (Daniel Pupius)
+ */
+
+goog.provide('goog.events.EventHandler');
+
+goog.require('goog.Disposable');
+goog.require('goog.events');
+goog.require('goog.object');
+
+goog.forwardDeclare('goog.events.EventWrapper');
+
+
+
+/**
+ * Super class for objects that want to easily manage a number of event
+ * listeners.  It allows a short cut to listen and also provides a quick way
+ * to remove all events listeners belonging to this object.
+ * @param {SCOPE=} opt_scope Object in whose scope to call the listeners.
+ * @constructor
+ * @extends {goog.Disposable}
+ * @template SCOPE
+ */
+goog.events.EventHandler = function(opt_scope) {
+  goog.Disposable.call(this);
+  // TODO(mknichel): Rename this to this.scope_ and fix the classes in google3
+  // that access this private variable. :(
+  this.handler_ = opt_scope;
+
+  /**
+   * Keys for events that are being listened to.
+   * @type {!Object<!goog.events.Key>}
+   * @private
+   */
+  this.keys_ = {};
+};
+goog.inherits(goog.events.EventHandler, goog.Disposable);
+
+
+/**
+ * Utility array used to unify the cases of listening for an array of types
+ * and listening for a single event, without using recursion or allocating
+ * an array each time.
+ * @type {!Array<string>}
+ * @const
+ * @private
+ */
+goog.events.EventHandler.typeArray_ = [];
+
+
+/**
+ * Listen to an event on a Listenable.  If the function is omitted then the
+ * EventHandler's handleEvent method will be used.
+ * @param {goog.events.ListenableType} src Event source.
+ * @param {string|Array<string>|
+ *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
+ *     type Event type to listen for or array of event types.
+ * @param {function(this:SCOPE, EVENTOBJ):?|{handleEvent:function(?):?}|null=}
+ *     opt_fn Optional callback function to be used as the listener or an object
+ *     with handleEvent function.
+ * @param {(boolean|!AddEventListenerOptions)=} opt_options
+ * @return {THIS} This object, allowing for chaining of calls.
+ * @this {THIS}
+ * @template EVENTOBJ, THIS
+ */
+goog.events.EventHandler.prototype.listen = function(
+    src, type, opt_fn, opt_options) {
+  var self = /** @type {!goog.events.EventHandler} */ (this);
+  return self.listen_(src, type, opt_fn, opt_options);
+};
+
+
+/**
+ * Listen to an event on a Listenable.  If the function is omitted then the
+ * EventHandler's handleEvent method will be used.
+ * @param {goog.events.ListenableType} src Event source.
+ * @param {string|Array<string>|
+ *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
+ *     type Event type to listen for or array of event types.
+ * @param {function(this:T, EVENTOBJ):?|{handleEvent:function(this:T, ?):?}|
+ *     null|undefined} fn Optional callback function to be used as the
+ *     listener or an object with handleEvent function.
+ * @param {boolean|!AddEventListenerOptions|undefined} options
+ * @param {T} scope Object in whose scope to call the listener.
+ * @return {THIS} This object, allowing for chaining of calls.
+ * @this {THIS}
+ * @template T, EVENTOBJ, THIS
+ */
+goog.events.EventHandler.prototype.listenWithScope = function(
+    src, type, fn, options, scope) {
+  var self = /** @type {!goog.events.EventHandler} */ (this);
+  // TODO(mknichel): Deprecate this function.
+  return self.listen_(src, type, fn, options, scope);
+};
+
+
+/**
+ * Listen to an event on a Listenable.  If the function is omitted then the
+ * EventHandler's handleEvent method will be used.
+ * @param {goog.events.ListenableType} src Event source.
+ * @param {string|Array<string>|
+ *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
+ *     type Event type to listen for or array of event types.
+ * @param {function(EVENTOBJ):?|{handleEvent:function(?):?}|null=} opt_fn
+ *     Optional callback function to be used as the listener or an object with
+ *     handleEvent function.
+ * @param {(boolean|!AddEventListenerOptions)=} opt_options
+ * @param {Object=} opt_scope Object in whose scope to call the listener.
+ * @return {THIS} This object, allowing for chaining of calls.
+ * @this {THIS}
+ * @template EVENTOBJ, THIS
+ * @private
+ */
+goog.events.EventHandler.prototype.listen_ = function(
+    src, type, opt_fn, opt_options, opt_scope) {
+  var self = /** @type {!goog.events.EventHandler} */ (this);
+  if (!goog.isArray(type)) {
+    if (type) {
+      goog.events.EventHandler.typeArray_[0] = type.toString();
+    }
+    type = goog.events.EventHandler.typeArray_;
+  }
+  for (var i = 0; i < type.length; i++) {
+    var listenerObj = goog.events.listen(
+        src, type[i], opt_fn || self.handleEvent, opt_options || false,
+        opt_scope || self.handler_ || self);
+
+    if (!listenerObj) {
+      // When goog.events.listen run on OFF_AND_FAIL or OFF_AND_SILENT
+      // (goog.events.CaptureSimulationMode) in IE8-, it will return null
+      // value.
+      return self;
+    }
+
+    var key = listenerObj.key;
+    self.keys_[key] = listenerObj;
+  }
+
+  return self;
+};
+
+
+/**
+ * Listen to an event on a Listenable.  If the function is omitted, then the
+ * EventHandler's handleEvent method will be used. After the event has fired the
+ * event listener is removed from the target. If an array of event types is
+ * provided, each event type will be listened to once.
+ * @param {goog.events.ListenableType} src Event source.
+ * @param {string|Array<string>|
+ *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
+ *     type Event type to listen for or array of event types.
+ * @param {function(this:SCOPE, EVENTOBJ):?|{handleEvent:function(?):?}|null=}
+ * opt_fn
+ *    Optional callback function to be used as the listener or an object with
+ *    handleEvent function.
+ * @param {(boolean|!AddEventListenerOptions)=} opt_options
+ * @return {THIS} This object, allowing for chaining of calls.
+ * @this {THIS}
+ * @template EVENTOBJ, THIS
+ */
+goog.events.EventHandler.prototype.listenOnce = function(
+    src, type, opt_fn, opt_options) {
+  var self = /** @type {!goog.events.EventHandler} */ (this);
+  return self.listenOnce_(src, type, opt_fn, opt_options);
+};
+
+
+/**
+ * Listen to an event on a Listenable.  If the function is omitted, then the
+ * EventHandler's handleEvent method will be used. After the event has fired the
+ * event listener is removed from the target. If an array of event types is
+ * provided, each event type will be listened to once.
+ * @param {goog.events.ListenableType} src Event source.
+ * @param {string|Array<string>|
+ *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
+ *     type Event type to listen for or array of event types.
+ * @param {function(this:T, EVENTOBJ):?|{handleEvent:function(this:T, ?):?}|
+ *     null|undefined} fn Optional callback function to be used as the
+ *     listener or an object with handleEvent function.
+ * @param {boolean|undefined} capture Optional whether to use capture phase.
+ * @param {T} scope Object in whose scope to call the listener.
+ * @return {THIS} This object, allowing for chaining of calls.
+ * @this {THIS}
+ * @template T, EVENTOBJ, THIS
+ */
+goog.events.EventHandler.prototype.listenOnceWithScope = function(
+    src, type, fn, capture, scope) {
+  var self = /** @type {!goog.events.EventHandler} */ (this);
+  // TODO(mknichel): Deprecate this function.
+  return self.listenOnce_(src, type, fn, capture, scope);
+};
+
+
+/**
+ * Listen to an event on a Listenable.  If the function is omitted, then the
+ * EventHandler's handleEvent method will be used. After the event has fired
+ * the event listener is removed from the target. If an array of event types is
+ * provided, each event type will be listened to once.
+ * @param {goog.events.ListenableType} src Event source.
+ * @param {string|Array<string>|
+ *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
+ *     type Event type to listen for or array of event types.
+ * @param {function(EVENTOBJ):?|{handleEvent:function(?):?}|null=} opt_fn
+ *    Optional callback function to be used as the listener or an object with
+ *    handleEvent function.
+ * @param {(boolean|!AddEventListenerOptions)=} opt_options
+ * @param {Object=} opt_scope Object in whose scope to call the listener.
+ * @return {THIS} This object, allowing for chaining of calls.
+ * @this {THIS}
+ * @template EVENTOBJ, THIS
+ * @private
+ */
+goog.events.EventHandler.prototype.listenOnce_ = function(
+    src, type, opt_fn, opt_options, opt_scope) {
+  var self = /** @type {!goog.events.EventHandler} */ (this);
+  if (goog.isArray(type)) {
+    for (var i = 0; i < type.length; i++) {
+      self.listenOnce_(src, type[i], opt_fn, opt_options, opt_scope);
+    }
+  } else {
+    var listenerObj = goog.events.listenOnce(
+        src, type, opt_fn || self.handleEvent, opt_options,
+        opt_scope || self.handler_ || self);
+    if (!listenerObj) {
+      // When goog.events.listen run on OFF_AND_FAIL or OFF_AND_SILENT
+      // (goog.events.CaptureSimulationMode) in IE8-, it will return null
+      // value.
+      return self;
+    }
+
+    var key = listenerObj.key;
+    self.keys_[key] = listenerObj;
+  }
+
+  return self;
+};
+
+
+/**
+ * Adds an event listener with a specific event wrapper on a DOM Node or an
+ * object that has implemented {@link goog.events.EventTarget}. A listener can
+ * only be added once to an object.
+ *
+ * @param {EventTarget|goog.events.EventTarget} src The node to listen to
+ *     events on.
+ * @param {goog.events.EventWrapper} wrapper Event wrapper to use.
+ * @param {function(this:SCOPE, ?):?|{handleEvent:function(?):?}|null} listener
+ *     Callback method, or an object with a handleEvent function.
+ * @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
+ *     false).
+ * @return {THIS} This object, allowing for chaining of calls.
+ * @this {THIS}
+ * @template THIS
+ */
+goog.events.EventHandler.prototype.listenWithWrapper = function(
+    src, wrapper, listener, opt_capt) {
+  var self = /** @type {!goog.events.EventHandler} */ (this);
+  // TODO(mknichel): Remove the opt_scope from this function and then
+  // templatize it.
+  return self.listenWithWrapper_(src, wrapper, listener, opt_capt);
+};
+
+
+/**
+ * Adds an event listener with a specific event wrapper on a DOM Node or an
+ * object that has implemented {@link goog.events.EventTarget}. A listener can
+ * only be added once to an object.
+ *
+ * @param {EventTarget|goog.events.EventTarget} src The node to listen to
+ *     events on.
+ * @param {goog.events.EventWrapper} wrapper Event wrapper to use.
+ * @param {function(this:T, ?):?|{handleEvent:function(this:T, ?):?}|null}
+ *     listener Optional callback function to be used as the
+ *     listener or an object with handleEvent function.
+ * @param {boolean|undefined} capture Optional whether to use capture phase.
+ * @param {T} scope Object in whose scope to call the listener.
+ * @return {THIS} This object, allowing for chaining of calls.
+ * @this {THIS}
+ * @template T, THIS
+ */
+goog.events.EventHandler.prototype.listenWithWrapperAndScope = function(
+    src, wrapper, listener, capture, scope) {
+  var self = /** @type {!goog.events.EventHandler} */ (this);
+  // TODO(mknichel): Deprecate this function.
+  return self.listenWithWrapper_(src, wrapper, listener, capture, scope);
+};
+
+
+/**
+ * Adds an event listener with a specific event wrapper on a DOM Node or an
+ * object that has implemented {@link goog.events.EventTarget}. A listener can
+ * only be added once to an object.
+ *
+ * @param {EventTarget|goog.events.EventTarget} src The node to listen to
+ *     events on.
+ * @param {goog.events.EventWrapper} wrapper Event wrapper to use.
+ * @param {function(?):?|{handleEvent:function(?):?}|null} listener Callback
+ *     method, or an object with a handleEvent function.
+ * @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
+ *     false).
+ * @param {Object=} opt_scope Element in whose scope to call the listener.
+ * @return {THIS} This object, allowing for chaining of calls.
+ * @this {THIS}
+ * @template THIS
+ * @private
+ */
+goog.events.EventHandler.prototype.listenWithWrapper_ = function(
+    src, wrapper, listener, opt_capt, opt_scope) {
+  var self = /** @type {!goog.events.EventHandler} */ (this);
+  wrapper.listen(
+      src, listener, opt_capt, opt_scope || self.handler_ || self, self);
+  return self;
+};
+
+
+/**
+ * @return {number} Number of listeners registered by this handler.
+ */
+goog.events.EventHandler.prototype.getListenerCount = function() {
+  var count = 0;
+  for (var key in this.keys_) {
+    if (Object.prototype.hasOwnProperty.call(this.keys_, key)) {
+      count++;
+    }
+  }
+  return count;
+};
+
+
+/**
+ * Unlistens on an event.
+ * @param {goog.events.ListenableType} src Event source.
+ * @param {string|Array<string>|
+ *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
+ *     type Event type or array of event types to unlisten to.
+ * @param {function(this:?, EVENTOBJ):?|{handleEvent:function(?):?}|null=}
+ *     opt_fn Optional callback function to be used as the listener or an object
+ *     with handleEvent function.
+ * @param {(boolean|!EventListenerOptions)=} opt_options
+ * @param {Object=} opt_scope Object in whose scope to call the listener.
+ * @return {THIS} This object, allowing for chaining of calls.
+ * @this {THIS}
+ * @template EVENTOBJ, THIS
+ */
+goog.events.EventHandler.prototype.unlisten = function(
+    src, type, opt_fn, opt_options, opt_scope) {
+  var self = /** @type {!goog.events.EventHandler} */ (this);
+  if (goog.isArray(type)) {
+    for (var i = 0; i < type.length; i++) {
+      self.unlisten(src, type[i], opt_fn, opt_options, opt_scope);
+    }
+  } else {
+    var capture =
+        goog.isObject(opt_options) ? !!opt_options.capture : !!opt_options;
+    var listener = goog.events.getListener(
+        src, type, opt_fn || self.handleEvent, capture,
+        opt_scope || self.handler_ || self);
+
+    if (listener) {
+      goog.events.unlistenByKey(listener);
+      delete self.keys_[listener.key];
+    }
+  }
+
+  return self;
+};
+
+
+/**
+ * Removes an event listener which was added with listenWithWrapper().
+ *
+ * @param {EventTarget|goog.events.EventTarget} src The target to stop
+ *     listening to events on.
+ * @param {goog.events.EventWrapper} wrapper Event wrapper to use.
+ * @param {function(?):?|{handleEvent:function(?):?}|null} listener The
+ *     listener function to remove.
+ * @param {boolean=} opt_capt In DOM-compliant browsers, this determines
+ *     whether the listener is fired during the capture or bubble phase of the
+ *     event.
+ * @param {Object=} opt_scope Element in whose scope to call the listener.
+ * @return {THIS} This object, allowing for chaining of calls.
+ * @this {THIS}
+ * @template THIS
+ */
+goog.events.EventHandler.prototype.unlistenWithWrapper = function(
+    src, wrapper, listener, opt_capt, opt_scope) {
+  var self = /** @type {!goog.events.EventHandler} */ (this);
+  wrapper.unlisten(
+      src, listener, opt_capt, opt_scope || self.handler_ || self, self);
+  return self;
+};
+
+
+/**
+ * Unlistens to all events.
+ */
+goog.events.EventHandler.prototype.removeAll = function() {
+  goog.object.forEach(this.keys_, function(listenerObj, key) {
+    if (this.keys_.hasOwnProperty(key)) {
+      goog.events.unlistenByKey(listenerObj);
+    }
+  }, this);
+
+  this.keys_ = {};
+};
+
+
+/**
+ * Disposes of this EventHandler and removes all listeners that it registered.
+ * @override
+ * @protected
+ */
+goog.events.EventHandler.prototype.disposeInternal = function() {
+  goog.events.EventHandler.superClass_.disposeInternal.call(this);
+  this.removeAll();
+};
+
+
+/**
+ * Default event handler
+ * @param {goog.events.Event} e Event object.
+ */
+goog.events.EventHandler.prototype.handleEvent = function(e) {
+  throw new Error('EventHandler.handleEvent not implemented');
+};
diff --git a/third_party/ink/closure/events/eventid.js b/third_party/ink/closure/events/eventid.js
new file mode 100644
index 0000000..9ff9e40
--- /dev/null
+++ b/third_party/ink/closure/events/eventid.js
@@ -0,0 +1,46 @@
+// Copyright 2013 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.
+
+goog.provide('goog.events.EventId');
+
+
+
+/**
+ * A templated class that is used when registering for events. Typical usage:
+ *
+ *    /** @type {goog.events.EventId<MyEventObj>} *\
+ *    var myEventId = new goog.events.EventId(
+ *        goog.events.getUniqueId(('someEvent'));
+ *
+ *    // No need to cast or declare here since the compiler knows the
+ *    // correct type of 'evt' (MyEventObj).
+ *    something.listen(myEventId, function(evt) {});
+ *
+ * @param {string} eventId
+ * @template T
+ * @constructor
+ * @struct
+ * @final
+ */
+goog.events.EventId = function(eventId) {
+  /** @const */ this.id = eventId;
+};
+
+
+/**
+ * @override
+ */
+goog.events.EventId.prototype.toString = function() {
+  return this.id;
+};
diff --git a/third_party/ink/closure/events/events.js b/third_party/ink/closure/events/events.js
new file mode 100644
index 0000000..4466923
--- /dev/null
+++ b/third_party/ink/closure/events/events.js
@@ -0,0 +1,1006 @@
+// Copyright 2005 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 An event manager for both native browser event
+ * targets and custom JavaScript event targets
+ * ({@code goog.events.Listenable}). This provides an abstraction
+ * over browsers' event systems.
+ *
+ * It also provides a simulation of W3C event model's capture phase in
+ * Internet Explorer (IE 8 and below). Caveat: the simulation does not
+ * interact well with listeners registered directly on the elements
+ * (bypassing goog.events) or even with listeners registered via
+ * goog.events in a separate JS binary. In these cases, we provide
+ * no ordering guarantees.
+ *
+ * The listeners will receive a "patched" event object. Such event object
+ * contains normalized values for certain event properties that differs in
+ * different browsers.
+ *
+ * Example usage:
+ * <pre>
+ * goog.events.listen(myNode, 'click', function(e) { alert('woo') });
+ * goog.events.listen(myNode, 'mouseover', mouseHandler, true);
+ * goog.events.unlisten(myNode, 'mouseover', mouseHandler, true);
+ * goog.events.removeAll(myNode);
+ * </pre>
+ *
+ * @author aa@google.com (Aaron Boodman) [Original implementation of listen()]
+ * @author pupius@google.com (Daniel Pupius) [Port to closure plus capture phase
+ *                                            in IE and event object patching]
+ * @author arv@google.com (Erik Arvidsson)
+ *
+ * @see ../demos/events.html
+ * @see ../demos/event-propagation.html
+ * @see ../demos/stopevent.html
+ */
+
+// IMPLEMENTATION NOTES:
+// goog.events stores an auxiliary data structure on each EventTarget
+// source being listened on. This allows us to take advantage of GC,
+// having the data structure GC'd when the EventTarget is GC'd. This
+// GC behavior is equivalent to using W3C DOM Events directly.
+
+goog.provide('goog.events');
+goog.provide('goog.events.CaptureSimulationMode');
+goog.provide('goog.events.Key');
+goog.provide('goog.events.ListenableType');
+
+goog.require('goog.asserts');
+goog.require('goog.debug.entryPointRegistry');
+goog.require('goog.events.BrowserEvent');
+goog.require('goog.events.BrowserFeature');
+goog.require('goog.events.Listenable');
+goog.require('goog.events.ListenerMap');
+
+goog.forwardDeclare('goog.debug.ErrorHandler');
+goog.forwardDeclare('goog.events.EventWrapper');
+
+
+/**
+ * @typedef {number|goog.events.ListenableKey}
+ */
+goog.events.Key;
+
+
+/**
+ * @typedef {EventTarget|goog.events.Listenable}
+ */
+goog.events.ListenableType;
+
+
+/**
+ * Property name on a native event target for the listener map
+ * associated with the event target.
+ * @private @const {string}
+ */
+goog.events.LISTENER_MAP_PROP_ = 'closure_lm_' + ((Math.random() * 1e6) | 0);
+
+
+/**
+ * String used to prepend to IE event types.
+ * @const
+ * @private
+ */
+goog.events.onString_ = 'on';
+
+
+/**
+ * Map of computed "on<eventname>" strings for IE event types. Caching
+ * this removes an extra object allocation in goog.events.listen which
+ * improves IE6 performance.
+ * @const
+ * @dict
+ * @private
+ */
+goog.events.onStringMap_ = {};
+
+
+/**
+ * @enum {number} Different capture simulation mode for IE8-.
+ */
+goog.events.CaptureSimulationMode = {
+  /**
+   * Does not perform capture simulation. Will asserts in IE8- when you
+   * add capture listeners.
+   */
+  OFF_AND_FAIL: 0,
+
+  /**
+   * Does not perform capture simulation, silently ignore capture
+   * listeners.
+   */
+  OFF_AND_SILENT: 1,
+
+  /**
+   * Performs capture simulation.
+   */
+  ON: 2
+};
+
+
+/**
+ * @define {number} The capture simulation mode for IE8-. By default,
+ *     this is ON.
+ */
+goog.define('goog.events.CAPTURE_SIMULATION_MODE', 2);
+
+
+/**
+ * Estimated count of total native listeners.
+ * @private {number}
+ */
+goog.events.listenerCountEstimate_ = 0;
+
+
+/**
+ * Adds an event listener for a specific event on a native event
+ * target (such as a DOM element) or an object that has implemented
+ * {@link goog.events.Listenable}. A listener can only be added once
+ * to an object and if it is added again the key for the listener is
+ * returned. Note that if the existing listener is a one-off listener
+ * (registered via listenOnce), it will no longer be a one-off
+ * listener after a call to listen().
+ *
+ * @param {EventTarget|goog.events.Listenable} src The node to listen
+ *     to events on.
+ * @param {string|Array<string>|
+ *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
+ *     type Event type or array of event types.
+ * @param {function(this:T, EVENTOBJ):?|{handleEvent:function(?):?}|null}
+ *     listener Callback method, or an object with a handleEvent function.
+ *     WARNING: passing an Object is now softly deprecated.
+ * @param {(boolean|!AddEventListenerOptions)=} opt_options
+ * @param {T=} opt_handler Element in whose scope to call the listener.
+ * @return {goog.events.Key} Unique key for the listener.
+ * @template T,EVENTOBJ
+ */
+goog.events.listen = function(src, type, listener, opt_options, opt_handler) {
+  if (opt_options && opt_options.once) {
+    return goog.events.listenOnce(
+        src, type, listener, opt_options, opt_handler);
+  }
+  if (goog.isArray(type)) {
+    for (var i = 0; i < type.length; i++) {
+      goog.events.listen(src, type[i], listener, opt_options, opt_handler);
+    }
+    return null;
+  }
+
+  listener = goog.events.wrapListener(listener);
+  if (goog.events.Listenable.isImplementedBy(src)) {
+    var capture =
+        goog.isObject(opt_options) ? !!opt_options.capture : !!opt_options;
+    return src.listen(
+        /** @type {string|!goog.events.EventId} */ (type), listener, capture,
+        opt_handler);
+  } else {
+    return goog.events.listen_(
+        /** @type {!EventTarget} */ (src), type, listener,
+        /* callOnce */ false, opt_options, opt_handler);
+  }
+};
+
+
+/**
+ * Adds an event listener for a specific event on a native event
+ * target. A listener can only be added once to an object and if it
+ * is added again the key for the listener is returned.
+ *
+ * Note that a one-off listener will not change an existing listener,
+ * if any. On the other hand a normal listener will change existing
+ * one-off listener to become a normal listener.
+ *
+ * @param {EventTarget} src The node to listen to events on.
+ * @param {string|?goog.events.EventId<EVENTOBJ>} type Event type.
+ * @param {!Function} listener Callback function.
+ * @param {boolean} callOnce Whether the listener is a one-off
+ *     listener or otherwise.
+ * @param {(boolean|!AddEventListenerOptions)=} opt_options
+ * @param {Object=} opt_handler Element in whose scope to call the listener.
+ * @return {goog.events.ListenableKey} Unique key for the listener.
+ * @template EVENTOBJ
+ * @private
+ */
+goog.events.listen_ = function(
+    src, type, listener, callOnce, opt_options, opt_handler) {
+  if (!type) {
+    throw new Error('Invalid event type');
+  }
+
+  var capture =
+      goog.isObject(opt_options) ? !!opt_options.capture : !!opt_options;
+  if (capture && !goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT) {
+    if (goog.events.CAPTURE_SIMULATION_MODE ==
+        goog.events.CaptureSimulationMode.OFF_AND_FAIL) {
+      goog.asserts.fail('Can not register capture listener in IE8-.');
+      return null;
+    } else if (
+        goog.events.CAPTURE_SIMULATION_MODE ==
+        goog.events.CaptureSimulationMode.OFF_AND_SILENT) {
+      return null;
+    }
+  }
+
+  var listenerMap = goog.events.getListenerMap_(src);
+  if (!listenerMap) {
+    src[goog.events.LISTENER_MAP_PROP_] = listenerMap =
+        new goog.events.ListenerMap(src);
+  }
+
+  var listenerObj = /** @type {goog.events.Listener} */ (
+      listenerMap.add(type, listener, callOnce, capture, opt_handler));
+
+  // If the listenerObj already has a proxy, it has been set up
+  // previously. We simply return.
+  if (listenerObj.proxy) {
+    return listenerObj;
+  }
+
+  var proxy = goog.events.getProxy();
+  listenerObj.proxy = proxy;
+
+  proxy.src = src;
+  proxy.listener = listenerObj;
+
+  // Attach the proxy through the browser's API
+  if (src.addEventListener) {
+    // Don't pass an object as `capture` if the browser doesn't support that.
+    if (!goog.events.BrowserFeature.PASSIVE_EVENTS) {
+      opt_options = capture;
+    }
+    // Don't break tests that expect a boolean.
+    if (opt_options === undefined) opt_options = false;
+    src.addEventListener(type.toString(), proxy, opt_options);
+  } else if (src.attachEvent) {
+    // The else if above used to be an unconditional else. It would call
+    // attachEvent come gws or high water. This would sometimes throw an
+    // exception on IE11, spoiling the day of some callers. The previous
+    // incarnation of this code, from 2007, indicates that it replaced an
+    // earlier still version that caused excess allocations on IE6.
+    src.attachEvent(goog.events.getOnString_(type.toString()), proxy);
+  } else {
+    throw new Error('addEventListener and attachEvent are unavailable.');
+  }
+
+  goog.events.listenerCountEstimate_++;
+  return listenerObj;
+};
+
+
+/**
+ * Helper function for returning a proxy function.
+ * @return {!Function} A new or reused function object.
+ */
+goog.events.getProxy = function() {
+  var proxyCallbackFunction = goog.events.handleBrowserEvent_;
+  // Use a local var f to prevent one allocation.
+  var f =
+      goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT ? function(eventObject) {
+        return proxyCallbackFunction.call(f.src, f.listener, eventObject);
+      } : function(eventObject) {
+        var v = proxyCallbackFunction.call(f.src, f.listener, eventObject);
+        // NOTE(chrishenry): In IE, we hack in a capture phase. However, if
+        // there is inline event handler which tries to prevent default (for
+        // example <a href="..." onclick="return false">...</a>) in a
+        // descendant element, the prevent default will be overridden
+        // by this listener if this listener were to return true. Hence, we
+        // return undefined.
+        if (!v) return v;
+      };
+  return f;
+};
+
+
+/**
+ * Adds an event listener for a specific event on a native event
+ * target (such as a DOM element) or an object that has implemented
+ * {@link goog.events.Listenable}. After the event has fired the event
+ * listener is removed from the target.
+ *
+ * If an existing listener already exists, listenOnce will do
+ * nothing. In particular, if the listener was previously registered
+ * via listen(), listenOnce() will not turn the listener into a
+ * one-off listener. Similarly, if there is already an existing
+ * one-off listener, listenOnce does not modify the listeners (it is
+ * still a once listener).
+ *
+ * @param {EventTarget|goog.events.Listenable} src The node to listen
+ *     to events on.
+ * @param {string|Array<string>|
+ *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
+ *     type Event type or array of event types.
+ * @param {function(this:T, EVENTOBJ):?|{handleEvent:function(?):?}|null}
+ *     listener Callback method.
+ * @param {(boolean|!AddEventListenerOptions)=} opt_options
+ * @param {T=} opt_handler Element in whose scope to call the listener.
+ * @return {goog.events.Key} Unique key for the listener.
+ * @template T,EVENTOBJ
+ */
+goog.events.listenOnce = function(
+    src, type, listener, opt_options, opt_handler) {
+  if (goog.isArray(type)) {
+    for (var i = 0; i < type.length; i++) {
+      goog.events.listenOnce(src, type[i], listener, opt_options, opt_handler);
+    }
+    return null;
+  }
+
+  listener = goog.events.wrapListener(listener);
+  if (goog.events.Listenable.isImplementedBy(src)) {
+    var capture =
+        goog.isObject(opt_options) ? !!opt_options.capture : !!opt_options;
+    return src.listenOnce(
+        /** @type {string|!goog.events.EventId} */ (type), listener, capture,
+        opt_handler);
+  } else {
+    return goog.events.listen_(
+        /** @type {!EventTarget} */ (src), type, listener,
+        /* callOnce */ true, opt_options, opt_handler);
+  }
+};
+
+
+/**
+ * Adds an event listener with a specific event wrapper on a DOM Node or an
+ * object that has implemented {@link goog.events.Listenable}. A listener can
+ * only be added once to an object.
+ *
+ * @param {EventTarget|goog.events.Listenable} src The target to
+ *     listen to events on.
+ * @param {goog.events.EventWrapper} wrapper Event wrapper to use.
+ * @param {function(this:T, ?):?|{handleEvent:function(?):?}|null} listener
+ *     Callback method, or an object with a handleEvent function.
+ * @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
+ *     false).
+ * @param {T=} opt_handler Element in whose scope to call the listener.
+ * @template T
+ */
+goog.events.listenWithWrapper = function(
+    src, wrapper, listener, opt_capt, opt_handler) {
+  wrapper.listen(src, listener, opt_capt, opt_handler);
+};
+
+
+/**
+ * Removes an event listener which was added with listen().
+ *
+ * @param {EventTarget|goog.events.Listenable} src The target to stop
+ *     listening to events on.
+ * @param {string|Array<string>|
+ *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
+ *     type Event type or array of event types to unlisten to.
+ * @param {function(?):?|{handleEvent:function(?):?}|null} listener The
+ *     listener function to remove.
+ * @param {(boolean|!EventListenerOptions)=} opt_options
+ *     whether the listener is fired during the capture or bubble phase of the
+ *     event.
+ * @param {Object=} opt_handler Element in whose scope to call the listener.
+ * @return {?boolean} indicating whether the listener was there to remove.
+ * @template EVENTOBJ
+ */
+goog.events.unlisten = function(src, type, listener, opt_options, opt_handler) {
+  if (goog.isArray(type)) {
+    for (var i = 0; i < type.length; i++) {
+      goog.events.unlisten(src, type[i], listener, opt_options, opt_handler);
+    }
+    return null;
+  }
+  var capture =
+      goog.isObject(opt_options) ? !!opt_options.capture : !!opt_options;
+
+  listener = goog.events.wrapListener(listener);
+  if (goog.events.Listenable.isImplementedBy(src)) {
+    return src.unlisten(
+        /** @type {string|!goog.events.EventId} */ (type), listener, capture,
+        opt_handler);
+  }
+
+  if (!src) {
+    // TODO(chrishenry): We should tighten the API to only accept
+    // non-null objects, or add an assertion here.
+    return false;
+  }
+
+  var listenerMap = goog.events.getListenerMap_(
+      /** @type {!EventTarget} */ (src));
+  if (listenerMap) {
+    var listenerObj = listenerMap.getListener(
+        /** @type {string|!goog.events.EventId} */ (type), listener, capture,
+        opt_handler);
+    if (listenerObj) {
+      return goog.events.unlistenByKey(listenerObj);
+    }
+  }
+
+  return false;
+};
+
+
+/**
+ * Removes an event listener which was added with listen() by the key
+ * returned by listen().
+ *
+ * @param {goog.events.Key} key The key returned by listen() for this
+ *     event listener.
+ * @return {boolean} indicating whether the listener was there to remove.
+ */
+goog.events.unlistenByKey = function(key) {
+  // TODO(chrishenry): Remove this check when tests that rely on this
+  // are fixed.
+  if (goog.isNumber(key)) {
+    return false;
+  }
+
+  var listener = key;
+  if (!listener || listener.removed) {
+    return false;
+  }
+
+  var src = listener.src;
+  if (goog.events.Listenable.isImplementedBy(src)) {
+    return /** @type {!goog.events.Listenable} */ (src).unlistenByKey(listener);
+  }
+
+  var type = listener.type;
+  var proxy = listener.proxy;
+  if (src.removeEventListener) {
+    src.removeEventListener(type, proxy, listener.capture);
+  } else if (src.detachEvent) {
+    src.detachEvent(goog.events.getOnString_(type), proxy);
+  }
+  goog.events.listenerCountEstimate_--;
+
+  var listenerMap = goog.events.getListenerMap_(
+      /** @type {!EventTarget} */ (src));
+  // TODO(chrishenry): Try to remove this conditional and execute the
+  // first branch always. This should be safe.
+  if (listenerMap) {
+    listenerMap.removeByKey(listener);
+    if (listenerMap.getTypeCount() == 0) {
+      // Null the src, just because this is simple to do (and useful
+      // for IE <= 7).
+      listenerMap.src = null;
+      // We don't use delete here because IE does not allow delete
+      // on a window object.
+      src[goog.events.LISTENER_MAP_PROP_] = null;
+    }
+  } else {
+    /** @type {!goog.events.Listener} */ (listener).markAsRemoved();
+  }
+
+  return true;
+};
+
+
+/**
+ * Removes an event listener which was added with listenWithWrapper().
+ *
+ * @param {EventTarget|goog.events.Listenable} src The target to stop
+ *     listening to events on.
+ * @param {goog.events.EventWrapper} wrapper Event wrapper to use.
+ * @param {function(?):?|{handleEvent:function(?):?}|null} listener The
+ *     listener function to remove.
+ * @param {boolean=} opt_capt In DOM-compliant browsers, this determines
+ *     whether the listener is fired during the capture or bubble phase of the
+ *     event.
+ * @param {Object=} opt_handler Element in whose scope to call the listener.
+ */
+goog.events.unlistenWithWrapper = function(
+    src, wrapper, listener, opt_capt, opt_handler) {
+  wrapper.unlisten(src, listener, opt_capt, opt_handler);
+};
+
+
+/**
+ * Removes all listeners from an object. You can also optionally
+ * remove listeners of a particular type.
+ *
+ * @param {Object|undefined} obj Object to remove listeners from. Must be an
+ *     EventTarget or a goog.events.Listenable.
+ * @param {string|!goog.events.EventId=} opt_type Type of event to remove.
+ *     Default is all types.
+ * @return {number} Number of listeners removed.
+ */
+goog.events.removeAll = function(obj, opt_type) {
+  // TODO(chrishenry): Change the type of obj to
+  // (!EventTarget|!goog.events.Listenable).
+
+  if (!obj) {
+    return 0;
+  }
+
+  if (goog.events.Listenable.isImplementedBy(obj)) {
+    return /** @type {?} */ (obj).removeAllListeners(opt_type);
+  }
+
+  var listenerMap = goog.events.getListenerMap_(
+      /** @type {!EventTarget} */ (obj));
+  if (!listenerMap) {
+    return 0;
+  }
+
+  var count = 0;
+  var typeStr = opt_type && opt_type.toString();
+  for (var type in listenerMap.listeners) {
+    if (!typeStr || type == typeStr) {
+      // Clone so that we don't need to worry about unlistenByKey
+      // changing the content of the ListenerMap.
+      var listeners = listenerMap.listeners[type].concat();
+      for (var i = 0; i < listeners.length; ++i) {
+        if (goog.events.unlistenByKey(listeners[i])) {
+          ++count;
+        }
+      }
+    }
+  }
+  return count;
+};
+
+
+/**
+ * Gets the listeners for a given object, type and capture phase.
+ *
+ * @param {Object} obj Object to get listeners for.
+ * @param {string|!goog.events.EventId} type Event type.
+ * @param {boolean} capture Capture phase?.
+ * @return {Array<!goog.events.Listener>} Array of listener objects.
+ */
+goog.events.getListeners = function(obj, type, capture) {
+  if (goog.events.Listenable.isImplementedBy(obj)) {
+    return /** @type {!goog.events.Listenable} */ (obj).getListeners(
+        type, capture);
+  } else {
+    if (!obj) {
+      // TODO(chrishenry): We should tighten the API to accept
+      // !EventTarget|goog.events.Listenable, and add an assertion here.
+      return [];
+    }
+
+    var listenerMap = goog.events.getListenerMap_(
+        /** @type {!EventTarget} */ (obj));
+    return listenerMap ? listenerMap.getListeners(type, capture) : [];
+  }
+};
+
+
+/**
+ * Gets the goog.events.Listener for the event or null if no such listener is
+ * in use.
+ *
+ * @param {EventTarget|goog.events.Listenable} src The target from
+ *     which to get listeners.
+ * @param {?string|!goog.events.EventId<EVENTOBJ>} type The type of the event.
+ * @param {function(EVENTOBJ):?|{handleEvent:function(?):?}|null} listener The
+ *     listener function to get.
+ * @param {boolean=} opt_capt In DOM-compliant browsers, this determines
+ *                            whether the listener is fired during the
+ *                            capture or bubble phase of the event.
+ * @param {Object=} opt_handler Element in whose scope to call the listener.
+ * @return {goog.events.ListenableKey} the found listener or null if not found.
+ * @template EVENTOBJ
+ */
+goog.events.getListener = function(src, type, listener, opt_capt, opt_handler) {
+  // TODO(chrishenry): Change type from ?string to string, or add assertion.
+  type = /** @type {string} */ (type);
+  listener = goog.events.wrapListener(listener);
+  var capture = !!opt_capt;
+  if (goog.events.Listenable.isImplementedBy(src)) {
+    return src.getListener(type, listener, capture, opt_handler);
+  }
+
+  if (!src) {
+    // TODO(chrishenry): We should tighten the API to only accept
+    // non-null objects, or add an assertion here.
+    return null;
+  }
+
+  var listenerMap = goog.events.getListenerMap_(
+      /** @type {!EventTarget} */ (src));
+  if (listenerMap) {
+    return listenerMap.getListener(type, listener, capture, opt_handler);
+  }
+  return null;
+};
+
+
+/**
+ * Returns whether an event target has any active listeners matching the
+ * specified signature. If either the type or capture parameters are
+ * unspecified, the function will match on the remaining criteria.
+ *
+ * @param {EventTarget|goog.events.Listenable} obj Target to get
+ *     listeners for.
+ * @param {string|!goog.events.EventId=} opt_type Event type.
+ * @param {boolean=} opt_capture Whether to check for capture or bubble-phase
+ *     listeners.
+ * @return {boolean} Whether an event target has one or more listeners matching
+ *     the requested type and/or capture phase.
+ */
+goog.events.hasListener = function(obj, opt_type, opt_capture) {
+  if (goog.events.Listenable.isImplementedBy(obj)) {
+    return obj.hasListener(opt_type, opt_capture);
+  }
+
+  var listenerMap = goog.events.getListenerMap_(
+      /** @type {!EventTarget} */ (obj));
+  return !!listenerMap && listenerMap.hasListener(opt_type, opt_capture);
+};
+
+
+/**
+ * Provides a nice string showing the normalized event objects public members
+ * @param {Object} e Event Object.
+ * @return {string} String of the public members of the normalized event object.
+ */
+goog.events.expose = function(e) {
+  var str = [];
+  for (var key in e) {
+    if (e[key] && e[key].id) {
+      str.push(key + ' = ' + e[key] + ' (' + e[key].id + ')');
+    } else {
+      str.push(key + ' = ' + e[key]);
+    }
+  }
+  return str.join('\n');
+};
+
+
+/**
+ * Returns a string with on prepended to the specified type. This is used for IE
+ * which expects "on" to be prepended. This function caches the string in order
+ * to avoid extra allocations in steady state.
+ * @param {string} type Event type.
+ * @return {string} The type string with 'on' prepended.
+ * @private
+ */
+goog.events.getOnString_ = function(type) {
+  if (type in goog.events.onStringMap_) {
+    return goog.events.onStringMap_[type];
+  }
+  return goog.events.onStringMap_[type] = goog.events.onString_ + type;
+};
+
+
+/**
+ * Fires an object's listeners of a particular type and phase
+ *
+ * @param {Object} obj Object whose listeners to call.
+ * @param {string|!goog.events.EventId} type Event type.
+ * @param {boolean} capture Which event phase.
+ * @param {Object} eventObject Event object to be passed to listener.
+ * @return {boolean} True if all listeners returned true else false.
+ */
+goog.events.fireListeners = function(obj, type, capture, eventObject) {
+  if (goog.events.Listenable.isImplementedBy(obj)) {
+    return /** @type {!goog.events.Listenable} */ (obj).fireListeners(
+        type, capture, eventObject);
+  }
+
+  return goog.events.fireListeners_(obj, type, capture, eventObject);
+};
+
+
+/**
+ * Fires an object's listeners of a particular type and phase.
+ * @param {Object} obj Object whose listeners to call.
+ * @param {string|!goog.events.EventId} type Event type.
+ * @param {boolean} capture Which event phase.
+ * @param {Object} eventObject Event object to be passed to listener.
+ * @return {boolean} True if all listeners returned true else false.
+ * @private
+ */
+goog.events.fireListeners_ = function(obj, type, capture, eventObject) {
+  /** @type {boolean} */
+  var retval = true;
+
+  var listenerMap = goog.events.getListenerMap_(
+      /** @type {EventTarget} */ (obj));
+  if (listenerMap) {
+    // TODO(chrishenry): Original code avoids array creation when there
+    // is no listener, so we do the same. If this optimization turns
+    // out to be not required, we can replace this with
+    // listenerMap.getListeners(type, capture) instead, which is simpler.
+    var listenerArray = listenerMap.listeners[type.toString()];
+    if (listenerArray) {
+      listenerArray = listenerArray.concat();
+      for (var i = 0; i < listenerArray.length; i++) {
+        var listener = listenerArray[i];
+        // We might not have a listener if the listener was removed.
+        if (listener && listener.capture == capture && !listener.removed) {
+          var result = goog.events.fireListener(listener, eventObject);
+          retval = retval && (result !== false);
+        }
+      }
+    }
+  }
+  return retval;
+};
+
+
+/**
+ * Fires a listener with a set of arguments
+ *
+ * @param {goog.events.Listener} listener The listener object to call.
+ * @param {Object} eventObject The event object to pass to the listener.
+ * @return {*} Result of listener.
+ */
+goog.events.fireListener = function(listener, eventObject) {
+  var listenerFn = listener.listener;
+  var listenerHandler = listener.handler || listener.src;
+
+  if (listener.callOnce) {
+    goog.events.unlistenByKey(listener);
+  }
+  return listenerFn.call(listenerHandler, eventObject);
+};
+
+
+/**
+ * Gets the total number of listeners currently in the system.
+ * @return {number} Number of listeners.
+ * @deprecated This returns estimated count, now that Closure no longer
+ * stores a central listener registry. We still return an estimation
+ * to keep existing listener-related tests passing. In the near future,
+ * this function will be removed.
+ */
+goog.events.getTotalListenerCount = function() {
+  return goog.events.listenerCountEstimate_;
+};
+
+
+/**
+ * Dispatches an event (or event like object) and calls all listeners
+ * listening for events of this type. The type of the event is decided by the
+ * type property on the event object.
+ *
+ * If any of the listeners returns false OR calls preventDefault then this
+ * function will return false.  If one of the capture listeners calls
+ * stopPropagation, then the bubble listeners won't fire.
+ *
+ * @param {goog.events.Listenable} src The event target.
+ * @param {goog.events.EventLike} e Event object.
+ * @return {boolean} If anyone called preventDefault on the event object (or
+ *     if any of the handlers returns false) this will also return false.
+ *     If there are no handlers, or if all handlers return true, this returns
+ *     true.
+ */
+goog.events.dispatchEvent = function(src, e) {
+  goog.asserts.assert(
+      goog.events.Listenable.isImplementedBy(src),
+      'Can not use goog.events.dispatchEvent with ' +
+          'non-goog.events.Listenable instance.');
+  return src.dispatchEvent(e);
+};
+
+
+/**
+ * Installs exception protection for the browser event entry point using the
+ * given error handler.
+ *
+ * @param {goog.debug.ErrorHandler} errorHandler Error handler with which to
+ *     protect the entry point.
+ */
+goog.events.protectBrowserEventEntryPoint = function(errorHandler) {
+  goog.events.handleBrowserEvent_ =
+      errorHandler.protectEntryPoint(goog.events.handleBrowserEvent_);
+};
+
+
+/**
+ * Handles an event and dispatches it to the correct listeners. This
+ * function is a proxy for the real listener the user specified.
+ *
+ * @param {goog.events.Listener} listener The listener object.
+ * @param {Event=} opt_evt Optional event object that gets passed in via the
+ *     native event handlers.
+ * @return {*} Result of the event handler.
+ * @this {EventTarget} The object or Element that fired the event.
+ * @private
+ */
+goog.events.handleBrowserEvent_ = function(listener, opt_evt) {
+  if (listener.removed) {
+    return true;
+  }
+
+  // Synthesize event propagation if the browser does not support W3C
+  // event model.
+  if (!goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT) {
+    var ieEvent = opt_evt ||
+        /** @type {Event} */ (goog.getObjectByName('window.event'));
+    var evt = new goog.events.BrowserEvent(ieEvent, this);
+    /** @type {*} */
+    var retval = true;
+
+    if (goog.events.CAPTURE_SIMULATION_MODE ==
+        goog.events.CaptureSimulationMode.ON) {
+      // If we have not marked this event yet, we should perform capture
+      // simulation.
+      if (!goog.events.isMarkedIeEvent_(ieEvent)) {
+        goog.events.markIeEvent_(ieEvent);
+
+        var ancestors = [];
+        for (var parent = evt.currentTarget; parent;
+             parent = parent.parentNode) {
+          ancestors.push(parent);
+        }
+
+        // Fire capture listeners.
+        var type = listener.type;
+        for (var i = ancestors.length - 1; !evt.propagationStopped_ && i >= 0;
+             i--) {
+          evt.currentTarget = ancestors[i];
+          var result =
+              goog.events.fireListeners_(ancestors[i], type, true, evt);
+          retval = retval && result;
+        }
+
+        // Fire bubble listeners.
+        //
+        // We can technically rely on IE to perform bubble event
+        // propagation. However, it turns out that IE fires events in
+        // opposite order of attachEvent registration, which broke
+        // some code and tests that rely on the order. (While W3C DOM
+        // Level 2 Events TR leaves the event ordering unspecified,
+        // modern browsers and W3C DOM Level 3 Events Working Draft
+        // actually specify the order as the registration order.)
+        for (var i = 0; !evt.propagationStopped_ && i < ancestors.length; i++) {
+          evt.currentTarget = ancestors[i];
+          var result =
+              goog.events.fireListeners_(ancestors[i], type, false, evt);
+          retval = retval && result;
+        }
+      }
+    } else {
+      retval = goog.events.fireListener(listener, evt);
+    }
+    return retval;
+  }
+
+  // Otherwise, simply fire the listener.
+  return goog.events.fireListener(
+      listener, new goog.events.BrowserEvent(opt_evt, this));
+};
+
+
+/**
+ * This is used to mark the IE event object so we do not do the Closure pass
+ * twice for a bubbling event.
+ * @param {Event} e The IE browser event.
+ * @private
+ */
+goog.events.markIeEvent_ = function(e) {
+  // Only the keyCode and the returnValue can be changed. We use keyCode for
+  // non keyboard events.
+  // event.returnValue is a bit more tricky. It is undefined by default. A
+  // boolean false prevents the default action. In a window.onbeforeunload and
+  // the returnValue is non undefined it will be alerted. However, we will only
+  // modify the returnValue for keyboard events. We can get a problem if non
+  // closure events sets the keyCode or the returnValue
+
+  var useReturnValue = false;
+
+  if (e.keyCode == 0) {
+    // We cannot change the keyCode in case that srcElement is input[type=file].
+    // We could test that that is the case but that would allocate 3 objects.
+    // If we use try/catch we will only allocate extra objects in the case of a
+    // failure.
+
+    try {
+      e.keyCode = -1;
+      return;
+    } catch (ex) {
+      useReturnValue = true;
+    }
+  }
+
+  if (useReturnValue ||
+      /** @type {boolean|undefined} */ (e.returnValue) == undefined) {
+    e.returnValue = true;
+  }
+};
+
+
+/**
+ * This is used to check if an IE event has already been handled by the Closure
+ * system so we do not do the Closure pass twice for a bubbling event.
+ * @param {Event} e  The IE browser event.
+ * @return {boolean} True if the event object has been marked.
+ * @private
+ */
+goog.events.isMarkedIeEvent_ = function(e) {
+  return e.keyCode < 0 || e.returnValue != undefined;
+};
+
+
+/**
+ * Counter to create unique event ids.
+ * @private {number}
+ */
+goog.events.uniqueIdCounter_ = 0;
+
+
+/**
+ * Creates a unique event id.
+ *
+ * @param {string} identifier The identifier.
+ * @return {string} A unique identifier.
+ * @idGenerator {unique}
+ */
+goog.events.getUniqueId = function(identifier) {
+  return identifier + '_' + goog.events.uniqueIdCounter_++;
+};
+
+
+/**
+ * @param {EventTarget} src The source object.
+ * @return {goog.events.ListenerMap} A listener map for the given
+ *     source object, or null if none exists.
+ * @private
+ */
+goog.events.getListenerMap_ = function(src) {
+  var listenerMap = src[goog.events.LISTENER_MAP_PROP_];
+  // IE serializes the property as well (e.g. when serializing outer
+  // HTML). So we must check that the value is of the correct type.
+  return listenerMap instanceof goog.events.ListenerMap ? listenerMap : null;
+};
+
+
+/**
+ * Expando property for listener function wrapper for Object with
+ * handleEvent.
+ * @private @const {string}
+ */
+goog.events.LISTENER_WRAPPER_PROP_ =
+    '__closure_events_fn_' + ((Math.random() * 1e9) >>> 0);
+
+
+/**
+ * @param {Object|Function} listener The listener function or an
+ *     object that contains handleEvent method.
+ * @return {!Function} Either the original function or a function that
+ *     calls obj.handleEvent. If the same listener is passed to this
+ *     function more than once, the same function is guaranteed to be
+ *     returned.
+ */
+goog.events.wrapListener = function(listener) {
+  goog.asserts.assert(listener, 'Listener can not be null.');
+
+  if (goog.isFunction(listener)) {
+    return listener;
+  }
+
+  goog.asserts.assert(
+      listener.handleEvent, 'An object listener must have handleEvent method.');
+  if (!listener[goog.events.LISTENER_WRAPPER_PROP_]) {
+    listener[goog.events.LISTENER_WRAPPER_PROP_] = function(e) {
+      return /** @type {?} */ (listener).handleEvent(e);
+    };
+  }
+  return listener[goog.events.LISTENER_WRAPPER_PROP_];
+};
+
+
+// Register the browser event handler as an entry point, so that
+// it can be monitored for exception handling, etc.
+goog.debug.entryPointRegistry.register(
+    /**
+     * @param {function(!Function): !Function} transformer The transforming
+     *     function.
+     */
+    function(transformer) {
+      goog.events.handleBrowserEvent_ =
+          transformer(goog.events.handleBrowserEvent_);
+    });
diff --git a/third_party/ink/closure/events/eventtarget.js b/third_party/ink/closure/events/eventtarget.js
new file mode 100644
index 0000000..b8adcaee
--- /dev/null
+++ b/third_party/ink/closure/events/eventtarget.js
@@ -0,0 +1,395 @@
+// Copyright 2005 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 A disposable implementation of a custom
+ * listenable/event target. See also: documentation for
+ * {@code goog.events.Listenable}.
+ *
+ * @author arv@google.com (Erik Arvidsson) [Original implementation]
+ * @author pupius@google.com (Daniel Pupius) [Port to use goog.events]
+ * @see ../demos/eventtarget.html
+ * @see goog.events.Listenable
+ */
+
+goog.provide('goog.events.EventTarget');
+
+goog.require('goog.Disposable');
+goog.require('goog.asserts');
+goog.require('goog.events');
+goog.require('goog.events.Event');
+goog.require('goog.events.Listenable');
+goog.require('goog.events.ListenerMap');
+goog.require('goog.object');
+
+
+
+/**
+ * An implementation of {@code goog.events.Listenable} with full W3C
+ * EventTarget-like support (capture/bubble mechanism, stopping event
+ * propagation, preventing default actions).
+ *
+ * You may subclass this class to turn your class into a Listenable.
+ *
+ * Unless propagation is stopped, an event dispatched by an
+ * EventTarget will bubble to the parent returned by
+ * {@code getParentEventTarget}. To set the parent, call
+ * {@code setParentEventTarget}. Subclasses that don't support
+ * changing the parent can override the setter to throw an error.
+ *
+ * Example usage:
+ * <pre>
+ *   var source = new goog.events.EventTarget();
+ *   function handleEvent(e) {
+ *     alert('Type: ' + e.type + '; Target: ' + e.target);
+ *   }
+ *   source.listen('foo', handleEvent);
+ *   // Or: goog.events.listen(source, 'foo', handleEvent);
+ *   ...
+ *   source.dispatchEvent('foo');  // will call handleEvent
+ *   ...
+ *   source.unlisten('foo', handleEvent);
+ *   // Or: goog.events.unlisten(source, 'foo', handleEvent);
+ * </pre>
+ *
+ * @constructor
+ * @extends {goog.Disposable}
+ * @implements {goog.events.Listenable}
+ */
+goog.events.EventTarget = function() {
+  goog.Disposable.call(this);
+
+  /**
+   * Maps of event type to an array of listeners.
+   * @private {!goog.events.ListenerMap}
+   */
+  this.eventTargetListeners_ = new goog.events.ListenerMap(this);
+
+  /**
+   * The object to use for event.target. Useful when mixing in an
+   * EventTarget to another object.
+   * @private {!Object}
+   */
+  this.actualEventTarget_ = this;
+
+  /**
+   * Parent event target, used during event bubbling.
+   *
+   * TODO(chrishenry): Change this to goog.events.Listenable. This
+   * currently breaks people who expect getParentEventTarget to return
+   * goog.events.EventTarget.
+   *
+   * @private {goog.events.EventTarget}
+   */
+  this.parentEventTarget_ = null;
+};
+goog.inherits(goog.events.EventTarget, goog.Disposable);
+goog.events.Listenable.addImplementation(goog.events.EventTarget);
+
+
+/**
+ * An artificial cap on the number of ancestors you can have. This is mainly
+ * for loop detection.
+ * @const {number}
+ * @private
+ */
+goog.events.EventTarget.MAX_ANCESTORS_ = 1000;
+
+
+/**
+ * Returns the parent of this event target to use for bubbling.
+ *
+ * @return {goog.events.EventTarget} The parent EventTarget or null if
+ *     there is no parent.
+ * @override
+ */
+goog.events.EventTarget.prototype.getParentEventTarget = function() {
+  return this.parentEventTarget_;
+};
+
+
+/**
+ * Sets the parent of this event target to use for capture/bubble
+ * mechanism.
+ * @param {goog.events.EventTarget} parent Parent listenable (null if none).
+ */
+goog.events.EventTarget.prototype.setParentEventTarget = function(parent) {
+  this.parentEventTarget_ = parent;
+};
+
+
+/**
+ * Adds an event listener to the event target. The same handler can only be
+ * added once per the type. Even if you add the same handler multiple times
+ * using the same type then it will only be called once when the event is
+ * dispatched.
+ *
+ * @param {string|!goog.events.EventId} type The type of the event to listen for
+ * @param {function(?):?|{handleEvent:function(?):?}|null} handler The function
+ *     to handle the event. The handler can also be an object that implements
+ *     the handleEvent method which takes the event object as argument.
+ * @param {boolean=} opt_capture In DOM-compliant browsers, this determines
+ *     whether the listener is fired during the capture or bubble phase
+ *     of the event.
+ * @param {Object=} opt_handlerScope Object in whose scope to call
+ *     the listener.
+ * @deprecated Use {@code #listen} instead, when possible. Otherwise, use
+ *     {@code goog.events.listen} if you are passing Object
+ *     (instead of Function) as handler.
+ */
+goog.events.EventTarget.prototype.addEventListener = function(
+    type, handler, opt_capture, opt_handlerScope) {
+  goog.events.listen(this, type, handler, opt_capture, opt_handlerScope);
+};
+
+
+/**
+ * Removes an event listener from the event target. The handler must be the
+ * same object as the one added. If the handler has not been added then
+ * nothing is done.
+ *
+ * @param {string} type The type of the event to listen for.
+ * @param {function(?):?|{handleEvent:function(?):?}|null} handler The function
+ *     to handle the event. The handler can also be an object that implements
+ *     the handleEvent method which takes the event object as argument.
+ * @param {boolean=} opt_capture In DOM-compliant browsers, this determines
+ *     whether the listener is fired during the capture or bubble phase
+ *     of the event.
+ * @param {Object=} opt_handlerScope Object in whose scope to call
+ *     the listener.
+ * @deprecated Use {@code #unlisten} instead, when possible. Otherwise, use
+ *     {@code goog.events.unlisten} if you are passing Object
+ *     (instead of Function) as handler.
+ */
+goog.events.EventTarget.prototype.removeEventListener = function(
+    type, handler, opt_capture, opt_handlerScope) {
+  goog.events.unlisten(this, type, handler, opt_capture, opt_handlerScope);
+};
+
+
+/** @override */
+goog.events.EventTarget.prototype.dispatchEvent = function(e) {
+  this.assertInitialized_();
+
+  var ancestorsTree, ancestor = this.getParentEventTarget();
+  if (ancestor) {
+    ancestorsTree = [];
+    var ancestorCount = 1;
+    for (; ancestor; ancestor = ancestor.getParentEventTarget()) {
+      ancestorsTree.push(ancestor);
+      goog.asserts.assert(
+          (++ancestorCount < goog.events.EventTarget.MAX_ANCESTORS_),
+          'infinite loop');
+    }
+  }
+
+  return goog.events.EventTarget.dispatchEventInternal_(
+      this.actualEventTarget_, e, ancestorsTree);
+};
+
+
+/**
+ * Removes listeners from this object.  Classes that extend EventTarget may
+ * need to override this method in order to remove references to DOM Elements
+ * and additional listeners.
+ * @override
+ */
+goog.events.EventTarget.prototype.disposeInternal = function() {
+  goog.events.EventTarget.superClass_.disposeInternal.call(this);
+
+  this.removeAllListeners();
+  this.parentEventTarget_ = null;
+};
+
+
+/** @override */
+goog.events.EventTarget.prototype.listen = function(
+    type, listener, opt_useCapture, opt_listenerScope) {
+  this.assertInitialized_();
+  return this.eventTargetListeners_.add(
+      String(type), listener, false /* callOnce */, opt_useCapture,
+      opt_listenerScope);
+};
+
+
+/** @override */
+goog.events.EventTarget.prototype.listenOnce = function(
+    type, listener, opt_useCapture, opt_listenerScope) {
+  return this.eventTargetListeners_.add(
+      String(type), listener, true /* callOnce */, opt_useCapture,
+      opt_listenerScope);
+};
+
+
+/** @override */
+goog.events.EventTarget.prototype.unlisten = function(
+    type, listener, opt_useCapture, opt_listenerScope) {
+  return this.eventTargetListeners_.remove(
+      String(type), listener, opt_useCapture, opt_listenerScope);
+};
+
+
+/** @override */
+goog.events.EventTarget.prototype.unlistenByKey = function(key) {
+  return this.eventTargetListeners_.removeByKey(key);
+};
+
+
+/** @override */
+goog.events.EventTarget.prototype.removeAllListeners = function(opt_type) {
+  // TODO(chrishenry): Previously, removeAllListeners can be called on
+  // uninitialized EventTarget, so we preserve that behavior. We
+  // should remove this when usages that rely on that fact are purged.
+  if (!this.eventTargetListeners_) {
+    return 0;
+  }
+  return this.eventTargetListeners_.removeAll(opt_type);
+};
+
+
+/** @override */
+goog.events.EventTarget.prototype.fireListeners = function(
+    type, capture, eventObject) {
+  // TODO(chrishenry): Original code avoids array creation when there
+  // is no listener, so we do the same. If this optimization turns
+  // out to be not required, we can replace this with
+  // getListeners(type, capture) instead, which is simpler.
+  var listenerArray = this.eventTargetListeners_.listeners[String(type)];
+  if (!listenerArray) {
+    return true;
+  }
+  listenerArray = listenerArray.concat();
+
+  var rv = true;
+  for (var i = 0; i < listenerArray.length; ++i) {
+    var listener = listenerArray[i];
+    // We might not have a listener if the listener was removed.
+    if (listener && !listener.removed && listener.capture == capture) {
+      var listenerFn = listener.listener;
+      var listenerHandler = listener.handler || listener.src;
+
+      if (listener.callOnce) {
+        this.unlistenByKey(listener);
+      }
+      rv = listenerFn.call(listenerHandler, eventObject) !== false && rv;
+    }
+  }
+
+  return rv && eventObject.returnValue_ != false;
+};
+
+
+/** @override */
+goog.events.EventTarget.prototype.getListeners = function(type, capture) {
+  return this.eventTargetListeners_.getListeners(String(type), capture);
+};
+
+
+/** @override */
+goog.events.EventTarget.prototype.getListener = function(
+    type, listener, capture, opt_listenerScope) {
+  return this.eventTargetListeners_.getListener(
+      String(type), listener, capture, opt_listenerScope);
+};
+
+
+/** @override */
+goog.events.EventTarget.prototype.hasListener = function(
+    opt_type, opt_capture) {
+  var id = goog.isDef(opt_type) ? String(opt_type) : undefined;
+  return this.eventTargetListeners_.hasListener(id, opt_capture);
+};
+
+
+/**
+ * Sets the target to be used for {@code event.target} when firing
+ * event. Mainly used for testing. For example, see
+ * {@code goog.testing.events.mixinListenable}.
+ * @param {!Object} target The target.
+ */
+goog.events.EventTarget.prototype.setTargetForTesting = function(target) {
+  this.actualEventTarget_ = target;
+};
+
+
+/**
+ * Asserts that the event target instance is initialized properly.
+ * @private
+ */
+goog.events.EventTarget.prototype.assertInitialized_ = function() {
+  goog.asserts.assert(
+      this.eventTargetListeners_,
+      'Event target is not initialized. Did you call the superclass ' +
+          '(goog.events.EventTarget) constructor?');
+};
+
+
+/**
+ * Dispatches the given event on the ancestorsTree.
+ *
+ * @param {!Object} target The target to dispatch on.
+ * @param {goog.events.Event|Object|string} e The event object.
+ * @param {Array<goog.events.Listenable>=} opt_ancestorsTree The ancestors
+ *     tree of the target, in reverse order from the closest ancestor
+ *     to the root event target. May be null if the target has no ancestor.
+ * @return {boolean} If anyone called preventDefault on the event object (or
+ *     if any of the listeners returns false) this will also return false.
+ * @private
+ */
+goog.events.EventTarget.dispatchEventInternal_ = function(
+    target, e, opt_ancestorsTree) {
+  var type = e.type || /** @type {string} */ (e);
+
+  // If accepting a string or object, create a custom event object so that
+  // preventDefault and stopPropagation work with the event.
+  if (goog.isString(e)) {
+    e = new goog.events.Event(e, target);
+  } else if (!(e instanceof goog.events.Event)) {
+    var oldEvent = e;
+    e = new goog.events.Event(type, target);
+    goog.object.extend(e, oldEvent);
+  } else {
+    e.target = e.target || target;
+  }
+
+  var rv = true, currentTarget;
+
+  // Executes all capture listeners on the ancestors, if any.
+  if (opt_ancestorsTree) {
+    for (var i = opt_ancestorsTree.length - 1; !e.propagationStopped_ && i >= 0;
+         i--) {
+      currentTarget = e.currentTarget = opt_ancestorsTree[i];
+      rv = currentTarget.fireListeners(type, true, e) && rv;
+    }
+  }
+
+  // Executes capture and bubble listeners on the target.
+  if (!e.propagationStopped_) {
+    currentTarget = /** @type {?} */ (e.currentTarget = target);
+    rv = currentTarget.fireListeners(type, true, e) && rv;
+    if (!e.propagationStopped_) {
+      rv = currentTarget.fireListeners(type, false, e) && rv;
+    }
+  }
+
+  // Executes all bubble listeners on the ancestors, if any.
+  if (opt_ancestorsTree) {
+    for (i = 0; !e.propagationStopped_ && i < opt_ancestorsTree.length; i++) {
+      currentTarget = e.currentTarget = opt_ancestorsTree[i];
+      rv = currentTarget.fireListeners(type, false, e) && rv;
+    }
+  }
+
+  return rv;
+};
diff --git a/third_party/ink/closure/events/eventtype.js b/third_party/ink/closure/events/eventtype.js
new file mode 100644
index 0000000..801d7f9
--- /dev/null
+++ b/third_party/ink/closure/events/eventtype.js
@@ -0,0 +1,360 @@
+// Copyright 2010 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 Event Types.
+ *
+ * @author arv@google.com (Erik Arvidsson)
+ * @author mirkov@google.com (Mirko Visontai)
+ */
+
+
+goog.provide('goog.events.EventType');
+goog.provide('goog.events.PointerFallbackEventType');
+
+goog.require('goog.events.BrowserFeature');
+goog.require('goog.userAgent');
+
+
+/**
+ * Returns a prefixed event name for the current browser.
+ * @param {string} eventName The name of the event.
+ * @return {string} The prefixed event name.
+ * @suppress {missingRequire|missingProvide}
+ * @private
+ */
+goog.events.getVendorPrefixedName_ = function(eventName) {
+  return goog.userAgent.WEBKIT ?
+      'webkit' + eventName :
+      (goog.userAgent.OPERA ? 'o' + eventName.toLowerCase() :
+                              eventName.toLowerCase());
+};
+
+
+/**
+ * Constants for event names.
+ * @enum {string}
+ */
+goog.events.EventType = {
+  // Mouse events
+  CLICK: 'click',
+  RIGHTCLICK: 'rightclick',
+  DBLCLICK: 'dblclick',
+  MOUSEDOWN: 'mousedown',
+  MOUSEUP: 'mouseup',
+  MOUSEOVER: 'mouseover',
+  MOUSEOUT: 'mouseout',
+  MOUSEMOVE: 'mousemove',
+  MOUSEENTER: 'mouseenter',
+  MOUSELEAVE: 'mouseleave',
+
+  // Selection events.
+  // https://www.w3.org/TR/selection-api/
+  SELECTIONCHANGE: 'selectionchange',
+  SELECTSTART: 'selectstart',  // IE, Safari, Chrome
+
+  // Wheel events
+  // http://www.w3.org/TR/DOM-Level-3-Events/#events-wheelevents
+  WHEEL: 'wheel',
+
+  // Key events
+  KEYPRESS: 'keypress',
+  KEYDOWN: 'keydown',
+  KEYUP: 'keyup',
+
+  // Focus
+  BLUR: 'blur',
+  FOCUS: 'focus',
+  DEACTIVATE: 'deactivate',  // IE only
+  // NOTE: The following two events are not stable in cross-browser usage.
+  //     WebKit and Opera implement DOMFocusIn/Out.
+  //     IE implements focusin/out.
+  //     Gecko implements neither see bug at
+  //     https://bugzilla.mozilla.org/show_bug.cgi?id=396927.
+  // The DOM Events Level 3 Draft deprecates DOMFocusIn in favor of focusin:
+  //     http://dev.w3.org/2006/webapi/DOM-Level-3-Events/html/DOM3-Events.html
+  // You can use FOCUS in Capture phase until implementations converge.
+  FOCUSIN: goog.userAgent.IE ? 'focusin' : 'DOMFocusIn',
+  FOCUSOUT: goog.userAgent.IE ? 'focusout' : 'DOMFocusOut',
+
+  // Forms
+  CHANGE: 'change',
+  RESET: 'reset',
+  SELECT: 'select',
+  SUBMIT: 'submit',
+  INPUT: 'input',
+  PROPERTYCHANGE: 'propertychange',  // IE only
+
+  // Drag and drop
+  DRAGSTART: 'dragstart',
+  DRAG: 'drag',
+  DRAGENTER: 'dragenter',
+  DRAGOVER: 'dragover',
+  DRAGLEAVE: 'dragleave',
+  DROP: 'drop',
+  DRAGEND: 'dragend',
+
+  // Touch events
+  // Note that other touch events exist, but we should follow the W3C list here.
+  // http://www.w3.org/TR/touch-events/#list-of-touchevent-types
+  TOUCHSTART: 'touchstart',
+  TOUCHMOVE: 'touchmove',
+  TOUCHEND: 'touchend',
+  TOUCHCANCEL: 'touchcancel',
+
+  // Misc
+  BEFOREUNLOAD: 'beforeunload',
+  CONSOLEMESSAGE: 'consolemessage',
+  CONTEXTMENU: 'contextmenu',
+  DEVICEMOTION: 'devicemotion',
+  DEVICEORIENTATION: 'deviceorientation',
+  DOMCONTENTLOADED: 'DOMContentLoaded',
+  ERROR: 'error',
+  HELP: 'help',
+  LOAD: 'load',
+  LOSECAPTURE: 'losecapture',
+  ORIENTATIONCHANGE: 'orientationchange',
+  READYSTATECHANGE: 'readystatechange',
+  RESIZE: 'resize',
+  SCROLL: 'scroll',
+  UNLOAD: 'unload',
+
+  // Media events
+  CANPLAY: 'canplay',
+  CANPLAYTHROUGH: 'canplaythrough',
+  DURATIONCHANGE: 'durationchange',
+  EMPTIED: 'emptied',
+  ENDED: 'ended',
+  LOADEDDATA: 'loadeddata',
+  LOADEDMETADATA: 'loadedmetadata',
+  PAUSE: 'pause',
+  PLAY: 'play',
+  PLAYING: 'playing',
+  RATECHANGE: 'ratechange',
+  SEEKED: 'seeked',
+  SEEKING: 'seeking',
+  STALLED: 'stalled',
+  SUSPEND: 'suspend',
+  TIMEUPDATE: 'timeupdate',
+  VOLUMECHANGE: 'volumechange',
+  WAITING: 'waiting',
+
+  // Media Source Extensions events
+  // https://www.w3.org/TR/media-source/#mediasource-events
+  SOURCEOPEN: 'sourceopen',
+  SOURCEENDED: 'sourceended',
+  SOURCECLOSED: 'sourceclosed',
+  // https://www.w3.org/TR/media-source/#sourcebuffer-events
+  ABORT: 'abort',
+  UPDATE: 'update',
+  UPDATESTART: 'updatestart',
+  UPDATEEND: 'updateend',
+
+  // HTML 5 History events
+  // See http://www.w3.org/TR/html5/browsers.html#event-definitions-0
+  HASHCHANGE: 'hashchange',
+  PAGEHIDE: 'pagehide',
+  PAGESHOW: 'pageshow',
+  POPSTATE: 'popstate',
+
+  // Copy and Paste
+  // Support is limited. Make sure it works on your favorite browser
+  // before using.
+  // http://www.quirksmode.org/dom/events/cutcopypaste.html
+  COPY: 'copy',
+  PASTE: 'paste',
+  CUT: 'cut',
+  BEFORECOPY: 'beforecopy',
+  BEFORECUT: 'beforecut',
+  BEFOREPASTE: 'beforepaste',
+
+  // HTML5 online/offline events.
+  // http://www.w3.org/TR/offline-webapps/#related
+  ONLINE: 'online',
+  OFFLINE: 'offline',
+
+  // HTML 5 worker events
+  MESSAGE: 'message',
+  CONNECT: 'connect',
+
+  // Service Worker Events - ServiceWorkerGlobalScope context
+  // See https://w3c.github.io/ServiceWorker/#execution-context-events
+  // Note: message event defined in worker events section
+  INSTALL: 'install',
+  ACTIVATE: 'activate',
+  FETCH: 'fetch',
+  FOREIGNFETCH: 'foreignfetch',
+  MESSAGEERROR: 'messageerror',
+
+  // Service Worker Events - Document context
+  // See https://w3c.github.io/ServiceWorker/#document-context-events
+  STATECHANGE: 'statechange',
+  UPDATEFOUND: 'updatefound',
+  CONTROLLERCHANGE: 'controllerchange',
+
+  // CSS animation events.
+  /** @suppress {missingRequire} */
+  ANIMATIONSTART: goog.events.getVendorPrefixedName_('AnimationStart'),
+  /** @suppress {missingRequire} */
+  ANIMATIONEND: goog.events.getVendorPrefixedName_('AnimationEnd'),
+  /** @suppress {missingRequire} */
+  ANIMATIONITERATION: goog.events.getVendorPrefixedName_('AnimationIteration'),
+
+  // CSS transition events. Based on the browser support described at:
+  // https://developer.mozilla.org/en/css/css_transitions#Browser_compatibility
+  /** @suppress {missingRequire} */
+  TRANSITIONEND: goog.events.getVendorPrefixedName_('TransitionEnd'),
+
+  // W3C Pointer Events
+  // http://www.w3.org/TR/pointerevents/
+  POINTERDOWN: 'pointerdown',
+  POINTERUP: 'pointerup',
+  POINTERCANCEL: 'pointercancel',
+  POINTERMOVE: 'pointermove',
+  POINTEROVER: 'pointerover',
+  POINTEROUT: 'pointerout',
+  POINTERENTER: 'pointerenter',
+  POINTERLEAVE: 'pointerleave',
+  GOTPOINTERCAPTURE: 'gotpointercapture',
+  LOSTPOINTERCAPTURE: 'lostpointercapture',
+
+  // IE specific events.
+  // See http://msdn.microsoft.com/en-us/library/ie/hh772103(v=vs.85).aspx
+  // Note: these events will be supplanted in IE11.
+  MSGESTURECHANGE: 'MSGestureChange',
+  MSGESTUREEND: 'MSGestureEnd',
+  MSGESTUREHOLD: 'MSGestureHold',
+  MSGESTURESTART: 'MSGestureStart',
+  MSGESTURETAP: 'MSGestureTap',
+  MSGOTPOINTERCAPTURE: 'MSGotPointerCapture',
+  MSINERTIASTART: 'MSInertiaStart',
+  MSLOSTPOINTERCAPTURE: 'MSLostPointerCapture',
+  MSPOINTERCANCEL: 'MSPointerCancel',
+  MSPOINTERDOWN: 'MSPointerDown',
+  MSPOINTERENTER: 'MSPointerEnter',
+  MSPOINTERHOVER: 'MSPointerHover',
+  MSPOINTERLEAVE: 'MSPointerLeave',
+  MSPOINTERMOVE: 'MSPointerMove',
+  MSPOINTEROUT: 'MSPointerOut',
+  MSPOINTEROVER: 'MSPointerOver',
+  MSPOINTERUP: 'MSPointerUp',
+
+  // Native IMEs/input tools events.
+  TEXT: 'text',
+  // The textInput event is supported in IE9+, but only in lower case. All other
+  // browsers use the camel-case event name.
+  TEXTINPUT: goog.userAgent.IE ? 'textinput' : 'textInput',
+  COMPOSITIONSTART: 'compositionstart',
+  COMPOSITIONUPDATE: 'compositionupdate',
+  COMPOSITIONEND: 'compositionend',
+
+  // The beforeinput event is initially only supported in Safari. See
+  // https://bugs.chromium.org/p/chromium/issues/detail?id=342670 for Chrome
+  // implementation tracking.
+  BEFOREINPUT: 'beforeinput',
+
+  // Webview tag events
+  // See http://developer.chrome.com/dev/apps/webview_tag.html
+  EXIT: 'exit',
+  LOADABORT: 'loadabort',
+  LOADCOMMIT: 'loadcommit',
+  LOADREDIRECT: 'loadredirect',
+  LOADSTART: 'loadstart',
+  LOADSTOP: 'loadstop',
+  RESPONSIVE: 'responsive',
+  SIZECHANGED: 'sizechanged',
+  UNRESPONSIVE: 'unresponsive',
+
+  // HTML5 Page Visibility API.  See details at
+  // {@code goog.labs.dom.PageVisibilityMonitor}.
+  VISIBILITYCHANGE: 'visibilitychange',
+
+  // LocalStorage event.
+  STORAGE: 'storage',
+
+  // DOM Level 2 mutation events (deprecated).
+  DOMSUBTREEMODIFIED: 'DOMSubtreeModified',
+  DOMNODEINSERTED: 'DOMNodeInserted',
+  DOMNODEREMOVED: 'DOMNodeRemoved',
+  DOMNODEREMOVEDFROMDOCUMENT: 'DOMNodeRemovedFromDocument',
+  DOMNODEINSERTEDINTODOCUMENT: 'DOMNodeInsertedIntoDocument',
+  DOMATTRMODIFIED: 'DOMAttrModified',
+  DOMCHARACTERDATAMODIFIED: 'DOMCharacterDataModified',
+
+  // Print events.
+  BEFOREPRINT: 'beforeprint',
+  AFTERPRINT: 'afterprint'
+};
+
+
+/**
+ * Returns one of the given pointer fallback event names in order of preference:
+ *   1. pointerEventName
+ *   2. msPointerEventName
+ *   3. mouseEventName
+ * @param {string} pointerEventName
+ * @param {string} msPointerEventName
+ * @param {string} mouseEventName
+ * @return {string} The supported pointer or mouse event name.
+ * @private
+ */
+goog.events.getPointerFallbackEventName_ = function(
+    pointerEventName, msPointerEventName, mouseEventName) {
+  if (goog.events.BrowserFeature.POINTER_EVENTS) {
+    return pointerEventName;
+  }
+  if (goog.events.BrowserFeature.MSPOINTER_EVENTS) {
+    return msPointerEventName;
+  }
+  return mouseEventName;
+};
+
+
+/**
+ * Constants for pointer event names that fall back to corresponding mouse event
+ * names on unsupported platforms. These are intended to be drop-in replacements
+ * for corresponding values in {@code goog.events.EventType}.
+ * @enum {string}
+ */
+goog.events.PointerFallbackEventType = {
+  POINTERDOWN: goog.events.getPointerFallbackEventName_(
+      goog.events.EventType.POINTERDOWN, goog.events.EventType.MSPOINTERDOWN,
+      goog.events.EventType.MOUSEDOWN),
+  POINTERUP: goog.events.getPointerFallbackEventName_(
+      goog.events.EventType.POINTERUP, goog.events.EventType.MSPOINTERUP,
+      goog.events.EventType.MOUSEUP),
+  POINTERCANCEL: goog.events.getPointerFallbackEventName_(
+      goog.events.EventType.POINTERCANCEL,
+      goog.events.EventType.MSPOINTERCANCEL,
+      // When falling back to mouse events, there is no MOUSECANCEL equivalent
+      // of POINTERCANCEL. In this case POINTERUP already falls back to MOUSEUP
+      // which represents both UP and CANCEL. POINTERCANCEL does not fall back
+      // to MOUSEUP to prevent listening twice on the same event.
+      'mousecancel'),  // non-existent event; will never fire
+  POINTERMOVE: goog.events.getPointerFallbackEventName_(
+      goog.events.EventType.POINTERMOVE, goog.events.EventType.MSPOINTERMOVE,
+      goog.events.EventType.MOUSEMOVE),
+  POINTEROVER: goog.events.getPointerFallbackEventName_(
+      goog.events.EventType.POINTEROVER, goog.events.EventType.MSPOINTEROVER,
+      goog.events.EventType.MOUSEOVER),
+  POINTEROUT: goog.events.getPointerFallbackEventName_(
+      goog.events.EventType.POINTEROUT, goog.events.EventType.MSPOINTEROUT,
+      goog.events.EventType.MOUSEOUT),
+  POINTERENTER: goog.events.getPointerFallbackEventName_(
+      goog.events.EventType.POINTERENTER, goog.events.EventType.MSPOINTERENTER,
+      goog.events.EventType.MOUSEENTER),
+  POINTERLEAVE: goog.events.getPointerFallbackEventName_(
+      goog.events.EventType.POINTERLEAVE, goog.events.EventType.MSPOINTERLEAVE,
+      goog.events.EventType.MOUSELEAVE)
+};
diff --git a/third_party/ink/closure/events/keycodes.js b/third_party/ink/closure/events/keycodes.js
new file mode 100644
index 0000000..745a6995
--- /dev/null
+++ b/third_party/ink/closure/events/keycodes.js
@@ -0,0 +1,439 @@
+// Copyright 2006 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 Constant declarations for common key codes.
+ *
+ * @author eae@google.com (Emil A Eklund)
+ * @see ../demos/keyhandler.html
+ */
+
+goog.provide('goog.events.KeyCodes');
+
+goog.require('goog.userAgent');
+
+goog.forwardDeclare('goog.events.BrowserEvent');
+
+
+/**
+ * Key codes for common characters.
+ *
+ * This list is not localized and therefore some of the key codes are not
+ * correct for non US keyboard layouts. See comments below.
+ *
+ * @enum {number}
+ */
+goog.events.KeyCodes = {
+  WIN_KEY_FF_LINUX: 0,
+  MAC_ENTER: 3,
+  BACKSPACE: 8,
+  TAB: 9,
+  NUM_CENTER: 12,  // NUMLOCK on FF/Safari Mac
+  ENTER: 13,
+  SHIFT: 16,
+  CTRL: 17,
+  ALT: 18,
+  PAUSE: 19,
+  CAPS_LOCK: 20,
+  ESC: 27,
+  SPACE: 32,
+  PAGE_UP: 33,    // also NUM_NORTH_EAST
+  PAGE_DOWN: 34,  // also NUM_SOUTH_EAST
+  END: 35,        // also NUM_SOUTH_WEST
+  HOME: 36,       // also NUM_NORTH_WEST
+  LEFT: 37,       // also NUM_WEST
+  UP: 38,         // also NUM_NORTH
+  RIGHT: 39,      // also NUM_EAST
+  DOWN: 40,       // also NUM_SOUTH
+  PLUS_SIGN: 43,  // NOT numpad plus
+  PRINT_SCREEN: 44,
+  INSERT: 45,  // also NUM_INSERT
+  DELETE: 46,  // also NUM_DELETE
+  ZERO: 48,
+  ONE: 49,
+  TWO: 50,
+  THREE: 51,
+  FOUR: 52,
+  FIVE: 53,
+  SIX: 54,
+  SEVEN: 55,
+  EIGHT: 56,
+  NINE: 57,
+  FF_SEMICOLON: 59,   // Firefox (Gecko) fires this for semicolon instead of 186
+  FF_EQUALS: 61,      // Firefox (Gecko) fires this for equals instead of 187
+  FF_DASH: 173,       // Firefox (Gecko) fires this for dash instead of 189
+  QUESTION_MARK: 63,  // needs localization
+  AT_SIGN: 64,
+  A: 65,
+  B: 66,
+  C: 67,
+  D: 68,
+  E: 69,
+  F: 70,
+  G: 71,
+  H: 72,
+  I: 73,
+  J: 74,
+  K: 75,
+  L: 76,
+  M: 77,
+  N: 78,
+  O: 79,
+  P: 80,
+  Q: 81,
+  R: 82,
+  S: 83,
+  T: 84,
+  U: 85,
+  V: 86,
+  W: 87,
+  X: 88,
+  Y: 89,
+  Z: 90,
+  META: 91,  // WIN_KEY_LEFT
+  WIN_KEY_RIGHT: 92,
+  CONTEXT_MENU: 93,
+  NUM_ZERO: 96,
+  NUM_ONE: 97,
+  NUM_TWO: 98,
+  NUM_THREE: 99,
+  NUM_FOUR: 100,
+  NUM_FIVE: 101,
+  NUM_SIX: 102,
+  NUM_SEVEN: 103,
+  NUM_EIGHT: 104,
+  NUM_NINE: 105,
+  NUM_MULTIPLY: 106,
+  NUM_PLUS: 107,
+  NUM_MINUS: 109,
+  NUM_PERIOD: 110,
+  NUM_DIVISION: 111,
+  F1: 112,
+  F2: 113,
+  F3: 114,
+  F4: 115,
+  F5: 116,
+  F6: 117,
+  F7: 118,
+  F8: 119,
+  F9: 120,
+  F10: 121,
+  F11: 122,
+  F12: 123,
+  NUMLOCK: 144,
+  SCROLL_LOCK: 145,
+
+  // OS-specific media keys like volume controls and browser controls.
+  FIRST_MEDIA_KEY: 166,
+  LAST_MEDIA_KEY: 183,
+
+  SEMICOLON: 186,             // needs localization
+  DASH: 189,                  // needs localization
+  EQUALS: 187,                // needs localization
+  COMMA: 188,                 // needs localization
+  PERIOD: 190,                // needs localization
+  SLASH: 191,                 // needs localization
+  APOSTROPHE: 192,            // needs localization
+  TILDE: 192,                 // needs localization
+  SINGLE_QUOTE: 222,          // needs localization
+  OPEN_SQUARE_BRACKET: 219,   // needs localization
+  BACKSLASH: 220,             // needs localization
+  CLOSE_SQUARE_BRACKET: 221,  // needs localization
+  WIN_KEY: 224,
+  MAC_FF_META:
+      224,  // Firefox (Gecko) fires this for the meta key instead of 91
+  MAC_WK_CMD_LEFT: 91,   // WebKit Left Command key fired, same as META
+  MAC_WK_CMD_RIGHT: 93,  // WebKit Right Command key fired, different from META
+  WIN_IME: 229,
+
+  // "Reserved for future use". Some programs (e.g. the SlingPlayer 2.4 ActiveX
+  // control) fire this as a hacky way to disable screensavers.
+  VK_NONAME: 252,
+
+  // We've seen users whose machines fire this keycode at regular one
+  // second intervals. The common thread among these users is that
+  // they're all using Dell Inspiron laptops, so we suspect that this
+  // indicates a hardware/bios problem.
+  // http://en.community.dell.com/support-forums/laptop/f/3518/p/19285957/19523128.aspx
+  PHANTOM: 255
+};
+
+
+/**
+ * Returns false if the event does not contain a text modifying key.
+ *
+ * When it returns true, the event might be text modifying. It is infeasible to
+ * say for sure because of the many different keyboard layouts, so this method
+ * errs on the side of assuming a key event is text-modifiable if we cannot be
+ * certain it is not. As an example, it will return true for ctrl+a, though in
+ * many standard keyboard layouts that key combination would mean "select all",
+ * and not actually modify the text.
+ *
+ * @param {goog.events.BrowserEvent} e A key event.
+ * @return {boolean} Whether it's a text modifying key.
+ */
+goog.events.KeyCodes.isTextModifyingKeyEvent = function(e) {
+  if (e.altKey && !e.ctrlKey || e.metaKey ||
+      // Function keys don't generate text
+      e.keyCode >= goog.events.KeyCodes.F1 &&
+          e.keyCode <= goog.events.KeyCodes.F12) {
+    return false;
+  }
+
+  // The following keys are quite harmless, even in combination with
+  // CTRL, ALT or SHIFT.
+  switch (e.keyCode) {
+    case goog.events.KeyCodes.ALT:
+    case goog.events.KeyCodes.CAPS_LOCK:
+    case goog.events.KeyCodes.CONTEXT_MENU:
+    case goog.events.KeyCodes.CTRL:
+    case goog.events.KeyCodes.DOWN:
+    case goog.events.KeyCodes.END:
+    case goog.events.KeyCodes.ESC:
+    case goog.events.KeyCodes.HOME:
+    case goog.events.KeyCodes.INSERT:
+    case goog.events.KeyCodes.LEFT:
+    case goog.events.KeyCodes.MAC_FF_META:
+    case goog.events.KeyCodes.META:
+    case goog.events.KeyCodes.NUMLOCK:
+    case goog.events.KeyCodes.NUM_CENTER:
+    case goog.events.KeyCodes.PAGE_DOWN:
+    case goog.events.KeyCodes.PAGE_UP:
+    case goog.events.KeyCodes.PAUSE:
+    case goog.events.KeyCodes.PHANTOM:
+    case goog.events.KeyCodes.PRINT_SCREEN:
+    case goog.events.KeyCodes.RIGHT:
+    case goog.events.KeyCodes.SCROLL_LOCK:
+    case goog.events.KeyCodes.SHIFT:
+    case goog.events.KeyCodes.UP:
+    case goog.events.KeyCodes.VK_NONAME:
+    case goog.events.KeyCodes.WIN_KEY:
+    case goog.events.KeyCodes.WIN_KEY_RIGHT:
+      return false;
+    case goog.events.KeyCodes.WIN_KEY_FF_LINUX:
+      return !goog.userAgent.GECKO;
+    default:
+      return e.keyCode < goog.events.KeyCodes.FIRST_MEDIA_KEY ||
+          e.keyCode > goog.events.KeyCodes.LAST_MEDIA_KEY;
+  }
+};
+
+
+/**
+ * Returns true if the key fires a keypress event in the current browser.
+ *
+ * Accoridng to MSDN [1] IE only fires keypress events for the following keys:
+ * - Letters: A - Z (uppercase and lowercase)
+ * - Numerals: 0 - 9
+ * - Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~
+ * - System: ESC, SPACEBAR, ENTER
+ *
+ * That's not entirely correct though, for instance there's no distinction
+ * between upper and lower case letters.
+ *
+ * [1] http://msdn2.microsoft.com/en-us/library/ms536939(VS.85).aspx)
+ *
+ * Safari is similar to IE, but does not fire keypress for ESC.
+ *
+ * Additionally, IE6 does not fire keydown or keypress events for letters when
+ * the control or alt keys are held down and the shift key is not. IE7 does
+ * fire keydown in these cases, though, but not keypress.
+ *
+ * @param {number} keyCode A key code.
+ * @param {number=} opt_heldKeyCode Key code of a currently-held key.
+ * @param {boolean=} opt_shiftKey Whether the shift key is held down.
+ * @param {boolean=} opt_ctrlKey Whether the control key is held down.
+ * @param {boolean=} opt_altKey Whether the alt key is held down.
+ * @param {boolean=} opt_metaKey Whether the meta key is held down.
+ * @return {boolean} Whether it's a key that fires a keypress event.
+ */
+goog.events.KeyCodes.firesKeyPressEvent = function(
+    keyCode, opt_heldKeyCode, opt_shiftKey, opt_ctrlKey, opt_altKey,
+    opt_metaKey) {
+  if (!goog.userAgent.IE && !goog.userAgent.EDGE &&
+      !(goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher('525'))) {
+    return true;
+  }
+
+  if (goog.userAgent.MAC && opt_altKey) {
+    return goog.events.KeyCodes.isCharacterKey(keyCode);
+  }
+
+  // Alt but not AltGr which is represented as Alt+Ctrl.
+  if (opt_altKey && !opt_ctrlKey) {
+    return false;
+  }
+
+  // Saves Ctrl or Alt + key for IE and WebKit 525+, which won't fire keypress.
+  // Non-IE browsers and WebKit prior to 525 won't get this far so no need to
+  // check the user agent.
+  if (goog.isNumber(opt_heldKeyCode)) {
+    opt_heldKeyCode = goog.events.KeyCodes.normalizeKeyCode(opt_heldKeyCode);
+  }
+  var heldKeyIsModifier = opt_heldKeyCode == goog.events.KeyCodes.CTRL ||
+      opt_heldKeyCode == goog.events.KeyCodes.ALT ||
+      goog.userAgent.MAC && opt_heldKeyCode == goog.events.KeyCodes.META;
+  // The Shift key blocks keypresses on Mac iff accompanied by another modifier.
+  var modifiedShiftKey = opt_heldKeyCode == goog.events.KeyCodes.SHIFT &&
+      (opt_ctrlKey || opt_metaKey);
+  if ((!opt_shiftKey || goog.userAgent.MAC) && heldKeyIsModifier ||
+      goog.userAgent.MAC && modifiedShiftKey) {
+    return false;
+  }
+
+  // Some keys with Ctrl/Shift do not issue keypress in WEBKIT.
+  if ((goog.userAgent.WEBKIT || goog.userAgent.EDGE) && opt_ctrlKey &&
+      opt_shiftKey) {
+    switch (keyCode) {
+      case goog.events.KeyCodes.BACKSLASH:
+      case goog.events.KeyCodes.OPEN_SQUARE_BRACKET:
+      case goog.events.KeyCodes.CLOSE_SQUARE_BRACKET:
+      case goog.events.KeyCodes.TILDE:
+      case goog.events.KeyCodes.SEMICOLON:
+      case goog.events.KeyCodes.DASH:
+      case goog.events.KeyCodes.EQUALS:
+      case goog.events.KeyCodes.COMMA:
+      case goog.events.KeyCodes.PERIOD:
+      case goog.events.KeyCodes.SLASH:
+      case goog.events.KeyCodes.APOSTROPHE:
+      case goog.events.KeyCodes.SINGLE_QUOTE:
+        return false;
+    }
+  }
+
+  // When Ctrl+<somekey> is held in IE, it only fires a keypress once, but it
+  // continues to fire keydown events as the event repeats.
+  if (goog.userAgent.IE && opt_ctrlKey && opt_heldKeyCode == keyCode) {
+    return false;
+  }
+
+  switch (keyCode) {
+    case goog.events.KeyCodes.ENTER:
+      return true;
+    case goog.events.KeyCodes.ESC:
+      return !(goog.userAgent.WEBKIT || goog.userAgent.EDGE);
+  }
+
+  return goog.events.KeyCodes.isCharacterKey(keyCode);
+};
+
+
+/**
+ * Returns true if the key produces a character.
+ * This does not cover characters on non-US keyboards (Russian, Hebrew, etc.).
+ *
+ * @param {number} keyCode A key code.
+ * @return {boolean} Whether it's a character key.
+ */
+goog.events.KeyCodes.isCharacterKey = function(keyCode) {
+  if (keyCode >= goog.events.KeyCodes.ZERO &&
+      keyCode <= goog.events.KeyCodes.NINE) {
+    return true;
+  }
+
+  if (keyCode >= goog.events.KeyCodes.NUM_ZERO &&
+      keyCode <= goog.events.KeyCodes.NUM_MULTIPLY) {
+    return true;
+  }
+
+  if (keyCode >= goog.events.KeyCodes.A && keyCode <= goog.events.KeyCodes.Z) {
+    return true;
+  }
+
+  // Safari sends zero key code for non-latin characters.
+  if ((goog.userAgent.WEBKIT || goog.userAgent.EDGE) && keyCode == 0) {
+    return true;
+  }
+
+  switch (keyCode) {
+    case goog.events.KeyCodes.SPACE:
+    case goog.events.KeyCodes.PLUS_SIGN:
+    case goog.events.KeyCodes.QUESTION_MARK:
+    case goog.events.KeyCodes.AT_SIGN:
+    case goog.events.KeyCodes.NUM_PLUS:
+    case goog.events.KeyCodes.NUM_MINUS:
+    case goog.events.KeyCodes.NUM_PERIOD:
+    case goog.events.KeyCodes.NUM_DIVISION:
+    case goog.events.KeyCodes.SEMICOLON:
+    case goog.events.KeyCodes.FF_SEMICOLON:
+    case goog.events.KeyCodes.DASH:
+    case goog.events.KeyCodes.EQUALS:
+    case goog.events.KeyCodes.FF_EQUALS:
+    case goog.events.KeyCodes.COMMA:
+    case goog.events.KeyCodes.PERIOD:
+    case goog.events.KeyCodes.SLASH:
+    case goog.events.KeyCodes.APOSTROPHE:
+    case goog.events.KeyCodes.SINGLE_QUOTE:
+    case goog.events.KeyCodes.OPEN_SQUARE_BRACKET:
+    case goog.events.KeyCodes.BACKSLASH:
+    case goog.events.KeyCodes.CLOSE_SQUARE_BRACKET:
+      return true;
+    default:
+      return false;
+  }
+};
+
+
+/**
+ * Normalizes key codes from OS/Browser-specific value to the general one.
+ * @param {number} keyCode The native key code.
+ * @return {number} The normalized key code.
+ */
+goog.events.KeyCodes.normalizeKeyCode = function(keyCode) {
+  if (goog.userAgent.GECKO) {
+    return goog.events.KeyCodes.normalizeGeckoKeyCode(keyCode);
+  } else if (goog.userAgent.MAC && goog.userAgent.WEBKIT) {
+    return goog.events.KeyCodes.normalizeMacWebKitKeyCode(keyCode);
+  } else {
+    return keyCode;
+  }
+};
+
+
+/**
+ * Normalizes key codes from their Gecko-specific value to the general one.
+ * @param {number} keyCode The native key code.
+ * @return {number} The normalized key code.
+ */
+goog.events.KeyCodes.normalizeGeckoKeyCode = function(keyCode) {
+  switch (keyCode) {
+    case goog.events.KeyCodes.FF_EQUALS:
+      return goog.events.KeyCodes.EQUALS;
+    case goog.events.KeyCodes.FF_SEMICOLON:
+      return goog.events.KeyCodes.SEMICOLON;
+    case goog.events.KeyCodes.FF_DASH:
+      return goog.events.KeyCodes.DASH;
+    case goog.events.KeyCodes.MAC_FF_META:
+      return goog.events.KeyCodes.META;
+    case goog.events.KeyCodes.WIN_KEY_FF_LINUX:
+      return goog.events.KeyCodes.WIN_KEY;
+    default:
+      return keyCode;
+  }
+};
+
+
+/**
+ * Normalizes key codes from their Mac WebKit-specific value to the general one.
+ * @param {number} keyCode The native key code.
+ * @return {number} The normalized key code.
+ */
+goog.events.KeyCodes.normalizeMacWebKitKeyCode = function(keyCode) {
+  switch (keyCode) {
+    case goog.events.KeyCodes.MAC_WK_CMD_RIGHT:  // 93
+      return goog.events.KeyCodes.META;          // 91
+    default:
+      return keyCode;
+  }
+};
diff --git a/third_party/ink/closure/events/listenable.js b/third_party/ink/closure/events/listenable.js
new file mode 100644
index 0000000..0f29d81
--- /dev/null
+++ b/third_party/ink/closure/events/listenable.js
@@ -0,0 +1,338 @@
+// Copyright 2012 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 An interface for a listenable JavaScript object.
+ * @author chrishenry@google.com (Chris Henry)
+ */
+
+goog.provide('goog.events.Listenable');
+goog.provide('goog.events.ListenableKey');
+
+/** @suppress {extraRequire} */
+goog.require('goog.events.EventId');
+
+goog.forwardDeclare('goog.events.EventLike');
+goog.forwardDeclare('goog.events.EventTarget');
+
+
+
+/**
+ * A listenable interface. A listenable is an object with the ability
+ * to dispatch/broadcast events to "event listeners" registered via
+ * listen/listenOnce.
+ *
+ * The interface allows for an event propagation mechanism similar
+ * to one offered by native browser event targets, such as
+ * capture/bubble mechanism, stopping propagation, and preventing
+ * default actions. Capture/bubble mechanism depends on the ancestor
+ * tree constructed via {@code #getParentEventTarget}; this tree
+ * must be directed acyclic graph. The meaning of default action(s)
+ * in preventDefault is specific to a particular use case.
+ *
+ * Implementations that do not support capture/bubble or can not have
+ * a parent listenable can simply not implement any ability to set the
+ * parent listenable (and have {@code #getParentEventTarget} return
+ * null).
+ *
+ * Implementation of this class can be used with or independently from
+ * goog.events.
+ *
+ * Implementation must call {@code #addImplementation(implClass)}.
+ *
+ * @interface
+ * @see goog.events
+ * @see http://www.w3.org/TR/DOM-Level-2-Events/events.html
+ */
+goog.events.Listenable = function() {};
+
+
+/**
+ * An expando property to indicate that an object implements
+ * goog.events.Listenable.
+ *
+ * See addImplementation/isImplementedBy.
+ *
+ * @type {string}
+ * @const
+ */
+goog.events.Listenable.IMPLEMENTED_BY_PROP =
+    'closure_listenable_' + ((Math.random() * 1e6) | 0);
+
+
+/**
+ * Marks a given class (constructor) as an implementation of
+ * Listenable, do that we can query that fact at runtime. The class
+ * must have already implemented the interface.
+ * @param {!function(new:goog.events.Listenable,...)} cls The class constructor.
+ *     The corresponding class must have already implemented the interface.
+ */
+goog.events.Listenable.addImplementation = function(cls) {
+  cls.prototype[goog.events.Listenable.IMPLEMENTED_BY_PROP] = true;
+};
+
+
+/**
+ * @param {Object} obj The object to check.
+ * @return {boolean} Whether a given instance implements Listenable. The
+ *     class/superclass of the instance must call addImplementation.
+ */
+goog.events.Listenable.isImplementedBy = function(obj) {
+  return !!(obj && obj[goog.events.Listenable.IMPLEMENTED_BY_PROP]);
+};
+
+
+/**
+ * Adds an event listener. A listener can only be added once to an
+ * object and if it is added again the key for the listener is
+ * returned. Note that if the existing listener is a one-off listener
+ * (registered via listenOnce), it will no longer be a one-off
+ * listener after a call to listen().
+ *
+ * @param {string|!goog.events.EventId<EVENTOBJ>} type The event type id.
+ * @param {function(this:SCOPE, EVENTOBJ):(boolean|undefined)} listener Callback
+ *     method.
+ * @param {boolean=} opt_useCapture Whether to fire in capture phase
+ *     (defaults to false).
+ * @param {SCOPE=} opt_listenerScope Object in whose scope to call the
+ *     listener.
+ * @return {!goog.events.ListenableKey} Unique key for the listener.
+ * @template SCOPE,EVENTOBJ
+ */
+goog.events.Listenable.prototype.listen;
+
+
+/**
+ * Adds an event listener that is removed automatically after the
+ * listener fired once.
+ *
+ * If an existing listener already exists, listenOnce will do
+ * nothing. In particular, if the listener was previously registered
+ * via listen(), listenOnce() will not turn the listener into a
+ * one-off listener. Similarly, if there is already an existing
+ * one-off listener, listenOnce does not modify the listeners (it is
+ * still a once listener).
+ *
+ * @param {string|!goog.events.EventId<EVENTOBJ>} type The event type id.
+ * @param {function(this:SCOPE, EVENTOBJ):(boolean|undefined)} listener Callback
+ *     method.
+ * @param {boolean=} opt_useCapture Whether to fire in capture phase
+ *     (defaults to false).
+ * @param {SCOPE=} opt_listenerScope Object in whose scope to call the
+ *     listener.
+ * @return {!goog.events.ListenableKey} Unique key for the listener.
+ * @template SCOPE,EVENTOBJ
+ */
+goog.events.Listenable.prototype.listenOnce;
+
+
+/**
+ * Removes an event listener which was added with listen() or listenOnce().
+ *
+ * @param {string|!goog.events.EventId<EVENTOBJ>} type The event type id.
+ * @param {function(this:SCOPE, EVENTOBJ):(boolean|undefined)} listener Callback
+ *     method.
+ * @param {boolean=} opt_useCapture Whether to fire in capture phase
+ *     (defaults to false).
+ * @param {SCOPE=} opt_listenerScope Object in whose scope to call
+ *     the listener.
+ * @return {boolean} Whether any listener was removed.
+ * @template SCOPE,EVENTOBJ
+ */
+goog.events.Listenable.prototype.unlisten;
+
+
+/**
+ * Removes an event listener which was added with listen() by the key
+ * returned by listen().
+ *
+ * @param {!goog.events.ListenableKey} key The key returned by
+ *     listen() or listenOnce().
+ * @return {boolean} Whether any listener was removed.
+ */
+goog.events.Listenable.prototype.unlistenByKey;
+
+
+/**
+ * Dispatches an event (or event like object) and calls all listeners
+ * listening for events of this type. The type of the event is decided by the
+ * type property on the event object.
+ *
+ * If any of the listeners returns false OR calls preventDefault then this
+ * function will return false.  If one of the capture listeners calls
+ * stopPropagation, then the bubble listeners won't fire.
+ *
+ * @param {goog.events.EventLike} e Event object.
+ * @return {boolean} If anyone called preventDefault on the event object (or
+ *     if any of the listeners returns false) this will also return false.
+ */
+goog.events.Listenable.prototype.dispatchEvent;
+
+
+/**
+ * Removes all listeners from this listenable. If type is specified,
+ * it will only remove listeners of the particular type. otherwise all
+ * registered listeners will be removed.
+ *
+ * @param {string=} opt_type Type of event to remove, default is to
+ *     remove all types.
+ * @return {number} Number of listeners removed.
+ */
+goog.events.Listenable.prototype.removeAllListeners;
+
+
+/**
+ * Returns the parent of this event target to use for capture/bubble
+ * mechanism.
+ *
+ * NOTE(chrishenry): The name reflects the original implementation of
+ * custom event target ({@code goog.events.EventTarget}). We decided
+ * that changing the name is not worth it.
+ *
+ * @return {goog.events.Listenable} The parent EventTarget or null if
+ *     there is no parent.
+ */
+goog.events.Listenable.prototype.getParentEventTarget;
+
+
+/**
+ * Fires all registered listeners in this listenable for the given
+ * type and capture mode, passing them the given eventObject. This
+ * does not perform actual capture/bubble. Only implementors of the
+ * interface should be using this.
+ *
+ * @param {string|!goog.events.EventId<EVENTOBJ>} type The type of the
+ *     listeners to fire.
+ * @param {boolean} capture The capture mode of the listeners to fire.
+ * @param {EVENTOBJ} eventObject The event object to fire.
+ * @return {boolean} Whether all listeners succeeded without
+ *     attempting to prevent default behavior. If any listener returns
+ *     false or called goog.events.Event#preventDefault, this returns
+ *     false.
+ * @template EVENTOBJ
+ */
+goog.events.Listenable.prototype.fireListeners;
+
+
+/**
+ * Gets all listeners in this listenable for the given type and
+ * capture mode.
+ *
+ * @param {string|!goog.events.EventId} type The type of the listeners to fire.
+ * @param {boolean} capture The capture mode of the listeners to fire.
+ * @return {!Array<!goog.events.ListenableKey>} An array of registered
+ *     listeners.
+ * @template EVENTOBJ
+ */
+goog.events.Listenable.prototype.getListeners;
+
+
+/**
+ * Gets the goog.events.ListenableKey for the event or null if no such
+ * listener is in use.
+ *
+ * @param {string|!goog.events.EventId<EVENTOBJ>} type The name of the event
+ *     without the 'on' prefix.
+ * @param {function(this:SCOPE, EVENTOBJ):(boolean|undefined)} listener The
+ *     listener function to get.
+ * @param {boolean} capture Whether the listener is a capturing listener.
+ * @param {SCOPE=} opt_listenerScope Object in whose scope to call the
+ *     listener.
+ * @return {goog.events.ListenableKey} the found listener or null if not found.
+ * @template SCOPE,EVENTOBJ
+ */
+goog.events.Listenable.prototype.getListener;
+
+
+/**
+ * Whether there is any active listeners matching the specified
+ * signature. If either the type or capture parameters are
+ * unspecified, the function will match on the remaining criteria.
+ *
+ * @param {string|!goog.events.EventId<EVENTOBJ>=} opt_type Event type.
+ * @param {boolean=} opt_capture Whether to check for capture or bubble
+ *     listeners.
+ * @return {boolean} Whether there is any active listeners matching
+ *     the requested type and/or capture phase.
+ * @template EVENTOBJ
+ */
+goog.events.Listenable.prototype.hasListener;
+
+
+
+/**
+ * An interface that describes a single registered listener.
+ * @interface
+ */
+goog.events.ListenableKey = function() {};
+
+
+/**
+ * Counter used to create a unique key
+ * @type {number}
+ * @private
+ */
+goog.events.ListenableKey.counter_ = 0;
+
+
+/**
+ * Reserves a key to be used for ListenableKey#key field.
+ * @return {number} A number to be used to fill ListenableKey#key
+ *     field.
+ */
+goog.events.ListenableKey.reserveKey = function() {
+  return ++goog.events.ListenableKey.counter_;
+};
+
+
+/**
+ * The source event target.
+ * @type {Object|goog.events.Listenable|goog.events.EventTarget}
+ */
+goog.events.ListenableKey.prototype.src;
+
+
+/**
+ * The event type the listener is listening to.
+ * @type {string}
+ */
+goog.events.ListenableKey.prototype.type;
+
+
+/**
+ * The listener function.
+ * @type {function(?):?|{handleEvent:function(?):?}|null}
+ */
+goog.events.ListenableKey.prototype.listener;
+
+
+/**
+ * Whether the listener works on capture phase.
+ * @type {boolean}
+ */
+goog.events.ListenableKey.prototype.capture;
+
+
+/**
+ * The 'this' object for the listener function's scope.
+ * @type {Object|undefined}
+ */
+goog.events.ListenableKey.prototype.handler;
+
+
+/**
+ * A globally unique number to identify the key.
+ * @type {number}
+ */
+goog.events.ListenableKey.prototype.key;
diff --git a/third_party/ink/closure/events/listener.js b/third_party/ink/closure/events/listener.js
new file mode 100644
index 0000000..282c69fc
--- /dev/null
+++ b/third_party/ink/closure/events/listener.js
@@ -0,0 +1,129 @@
+// Copyright 2005 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 Listener object.
+ * @author pupius@google.com (Daniel Pupius)
+ * @see ../demos/events.html
+ */
+
+goog.provide('goog.events.Listener');
+
+goog.require('goog.events.ListenableKey');
+
+
+
+/**
+ * Simple class that stores information about a listener
+ * @param {function(?):?} listener Callback function.
+ * @param {Function} proxy Wrapper for the listener that patches the event.
+ * @param {EventTarget|goog.events.Listenable} src Source object for
+ *     the event.
+ * @param {string} type Event type.
+ * @param {boolean} capture Whether in capture or bubble phase.
+ * @param {Object=} opt_handler Object in whose context to execute the callback.
+ * @implements {goog.events.ListenableKey}
+ * @constructor
+ */
+goog.events.Listener = function(
+    listener, proxy, src, type, capture, opt_handler) {
+  if (goog.events.Listener.ENABLE_MONITORING) {
+    this.creationStack = new Error().stack;
+  }
+
+  /** @override */
+  this.listener = listener;
+
+  /**
+   * A wrapper over the original listener. This is used solely to
+   * handle native browser events (it is used to simulate the capture
+   * phase and to patch the event object).
+   * @type {Function}
+   */
+  this.proxy = proxy;
+
+  /**
+   * Object or node that callback is listening to
+   * @type {EventTarget|goog.events.Listenable}
+   */
+  this.src = src;
+
+  /**
+   * The event type.
+   * @const {string}
+   */
+  this.type = type;
+
+  /**
+   * Whether the listener is being called in the capture or bubble phase
+   * @const {boolean}
+   */
+  this.capture = !!capture;
+
+  /**
+   * Optional object whose context to execute the listener in
+   * @type {Object|undefined}
+   */
+  this.handler = opt_handler;
+
+  /**
+   * The key of the listener.
+   * @const {number}
+   * @override
+   */
+  this.key = goog.events.ListenableKey.reserveKey();
+
+  /**
+   * Whether to remove the listener after it has been called.
+   * @type {boolean}
+   */
+  this.callOnce = false;
+
+  /**
+   * Whether the listener has been removed.
+   * @type {boolean}
+   */
+  this.removed = false;
+};
+
+
+/**
+ * @define {boolean} Whether to enable the monitoring of the
+ *     goog.events.Listener instances. Switching on the monitoring is only
+ *     recommended for debugging because it has a significant impact on
+ *     performance and memory usage. If switched off, the monitoring code
+ *     compiles down to 0 bytes.
+ */
+goog.define('goog.events.Listener.ENABLE_MONITORING', false);
+
+
+/**
+ * If monitoring the goog.events.Listener instances is enabled, stores the
+ * creation stack trace of the Disposable instance.
+ * @type {string}
+ */
+goog.events.Listener.prototype.creationStack;
+
+
+/**
+ * Marks this listener as removed. This also remove references held by
+ * this listener object (such as listener and event source).
+ */
+goog.events.Listener.prototype.markAsRemoved = function() {
+  this.removed = true;
+  this.listener = null;
+  this.proxy = null;
+  this.src = null;
+  this.handler = null;
+};
diff --git a/third_party/ink/closure/events/listenermap.js b/third_party/ink/closure/events/listenermap.js
new file mode 100644
index 0000000..30fea18
--- /dev/null
+++ b/third_party/ink/closure/events/listenermap.js
@@ -0,0 +1,307 @@
+// Copyright 2013 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 A map of listeners that provides utility functions to
+ * deal with listeners on an event target. Used by
+ * {@code goog.events.EventTarget}.
+ *
+ * WARNING: Do not use this class from outside goog.events package.
+ *
+ * @visibility {//javascript/closure/bin/sizetests:__pkg__}
+ * @visibility {//javascript/closure:__pkg__}
+ * @visibility {//javascript/closure/events:__pkg__}
+ * @visibility {//javascript/closure/labs/events:__pkg__}
+ */
+
+goog.provide('goog.events.ListenerMap');
+
+goog.require('goog.array');
+goog.require('goog.events.Listener');
+goog.require('goog.object');
+
+
+
+/**
+ * Creates a new listener map.
+ * @param {EventTarget|goog.events.Listenable} src The src object.
+ * @constructor
+ * @final
+ */
+goog.events.ListenerMap = function(src) {
+  /** @type {EventTarget|goog.events.Listenable} */
+  this.src = src;
+
+  /**
+   * Maps of event type to an array of listeners.
+   * @type {!Object<string, !Array<!goog.events.Listener>>}
+   */
+  this.listeners = {};
+
+  /**
+   * The count of types in this map that have registered listeners.
+   * @private {number}
+   */
+  this.typeCount_ = 0;
+};
+
+
+/**
+ * @return {number} The count of event types in this map that actually
+ *     have registered listeners.
+ */
+goog.events.ListenerMap.prototype.getTypeCount = function() {
+  return this.typeCount_;
+};
+
+
+/**
+ * @return {number} Total number of registered listeners.
+ */
+goog.events.ListenerMap.prototype.getListenerCount = function() {
+  var count = 0;
+  for (var type in this.listeners) {
+    count += this.listeners[type].length;
+  }
+  return count;
+};
+
+
+/**
+ * Adds an event listener. A listener can only be added once to an
+ * object and if it is added again the key for the listener is
+ * returned.
+ *
+ * Note that a one-off listener will not change an existing listener,
+ * if any. On the other hand a normal listener will change existing
+ * one-off listener to become a normal listener.
+ *
+ * @param {string|!goog.events.EventId} type The listener event type.
+ * @param {!Function} listener This listener callback method.
+ * @param {boolean} callOnce Whether the listener is a one-off
+ *     listener.
+ * @param {boolean=} opt_useCapture The capture mode of the listener.
+ * @param {Object=} opt_listenerScope Object in whose scope to call the
+ *     listener.
+ * @return {!goog.events.ListenableKey} Unique key for the listener.
+ */
+goog.events.ListenerMap.prototype.add = function(
+    type, listener, callOnce, opt_useCapture, opt_listenerScope) {
+  var typeStr = type.toString();
+  var listenerArray = this.listeners[typeStr];
+  if (!listenerArray) {
+    listenerArray = this.listeners[typeStr] = [];
+    this.typeCount_++;
+  }
+
+  var listenerObj;
+  var index = goog.events.ListenerMap.findListenerIndex_(
+      listenerArray, listener, opt_useCapture, opt_listenerScope);
+  if (index > -1) {
+    listenerObj = listenerArray[index];
+    if (!callOnce) {
+      // Ensure that, if there is an existing callOnce listener, it is no
+      // longer a callOnce listener.
+      listenerObj.callOnce = false;
+    }
+  } else {
+    listenerObj = new goog.events.Listener(
+        listener, null, this.src, typeStr, !!opt_useCapture, opt_listenerScope);
+    listenerObj.callOnce = callOnce;
+    listenerArray.push(listenerObj);
+  }
+  return listenerObj;
+};
+
+
+/**
+ * Removes a matching listener.
+ * @param {string|!goog.events.EventId} type The listener event type.
+ * @param {!Function} listener This listener callback method.
+ * @param {boolean=} opt_useCapture The capture mode of the listener.
+ * @param {Object=} opt_listenerScope Object in whose scope to call the
+ *     listener.
+ * @return {boolean} Whether any listener was removed.
+ */
+goog.events.ListenerMap.prototype.remove = function(
+    type, listener, opt_useCapture, opt_listenerScope) {
+  var typeStr = type.toString();
+  if (!(typeStr in this.listeners)) {
+    return false;
+  }
+
+  var listenerArray = this.listeners[typeStr];
+  var index = goog.events.ListenerMap.findListenerIndex_(
+      listenerArray, listener, opt_useCapture, opt_listenerScope);
+  if (index > -1) {
+    var listenerObj = listenerArray[index];
+    listenerObj.markAsRemoved();
+    goog.array.removeAt(listenerArray, index);
+    if (listenerArray.length == 0) {
+      delete this.listeners[typeStr];
+      this.typeCount_--;
+    }
+    return true;
+  }
+  return false;
+};
+
+
+/**
+ * Removes the given listener object.
+ * @param {!goog.events.ListenableKey} listener The listener to remove.
+ * @return {boolean} Whether the listener is removed.
+ */
+goog.events.ListenerMap.prototype.removeByKey = function(listener) {
+  var type = listener.type;
+  if (!(type in this.listeners)) {
+    return false;
+  }
+
+  var removed = goog.array.remove(this.listeners[type], listener);
+  if (removed) {
+    /** @type {!goog.events.Listener} */ (listener).markAsRemoved();
+    if (this.listeners[type].length == 0) {
+      delete this.listeners[type];
+      this.typeCount_--;
+    }
+  }
+  return removed;
+};
+
+
+/**
+ * Removes all listeners from this map. If opt_type is provided, only
+ * listeners that match the given type are removed.
+ * @param {string|!goog.events.EventId=} opt_type Type of event to remove.
+ * @return {number} Number of listeners removed.
+ */
+goog.events.ListenerMap.prototype.removeAll = function(opt_type) {
+  var typeStr = opt_type && opt_type.toString();
+  var count = 0;
+  for (var type in this.listeners) {
+    if (!typeStr || type == typeStr) {
+      var listenerArray = this.listeners[type];
+      for (var i = 0; i < listenerArray.length; i++) {
+        ++count;
+        listenerArray[i].markAsRemoved();
+      }
+      delete this.listeners[type];
+      this.typeCount_--;
+    }
+  }
+  return count;
+};
+
+
+/**
+ * Gets all listeners that match the given type and capture mode. The
+ * returned array is a copy (but the listener objects are not).
+ * @param {string|!goog.events.EventId} type The type of the listeners
+ *     to retrieve.
+ * @param {boolean} capture The capture mode of the listeners to retrieve.
+ * @return {!Array<!goog.events.ListenableKey>} An array of matching
+ *     listeners.
+ */
+goog.events.ListenerMap.prototype.getListeners = function(type, capture) {
+  var listenerArray = this.listeners[type.toString()];
+  var rv = [];
+  if (listenerArray) {
+    for (var i = 0; i < listenerArray.length; ++i) {
+      var listenerObj = listenerArray[i];
+      if (listenerObj.capture == capture) {
+        rv.push(listenerObj);
+      }
+    }
+  }
+  return rv;
+};
+
+
+/**
+ * Gets the goog.events.ListenableKey for the event or null if no such
+ * listener is in use.
+ *
+ * @param {string|!goog.events.EventId} type The type of the listener
+ *     to retrieve.
+ * @param {!Function} listener The listener function to get.
+ * @param {boolean} capture Whether the listener is a capturing listener.
+ * @param {Object=} opt_listenerScope Object in whose scope to call the
+ *     listener.
+ * @return {goog.events.ListenableKey} the found listener or null if not found.
+ */
+goog.events.ListenerMap.prototype.getListener = function(
+    type, listener, capture, opt_listenerScope) {
+  var listenerArray = this.listeners[type.toString()];
+  var i = -1;
+  if (listenerArray) {
+    i = goog.events.ListenerMap.findListenerIndex_(
+        listenerArray, listener, capture, opt_listenerScope);
+  }
+  return i > -1 ? listenerArray[i] : null;
+};
+
+
+/**
+ * Whether there is a matching listener. If either the type or capture
+ * parameters are unspecified, the function will match on the
+ * remaining criteria.
+ *
+ * @param {string|!goog.events.EventId=} opt_type The type of the listener.
+ * @param {boolean=} opt_capture The capture mode of the listener.
+ * @return {boolean} Whether there is an active listener matching
+ *     the requested type and/or capture phase.
+ */
+goog.events.ListenerMap.prototype.hasListener = function(
+    opt_type, opt_capture) {
+  var hasType = goog.isDef(opt_type);
+  var typeStr = hasType ? opt_type.toString() : '';
+  var hasCapture = goog.isDef(opt_capture);
+
+  return goog.object.some(this.listeners, function(listenerArray, type) {
+    for (var i = 0; i < listenerArray.length; ++i) {
+      if ((!hasType || listenerArray[i].type == typeStr) &&
+          (!hasCapture || listenerArray[i].capture == opt_capture)) {
+        return true;
+      }
+    }
+
+    return false;
+  });
+};
+
+
+/**
+ * Finds the index of a matching goog.events.Listener in the given
+ * listenerArray.
+ * @param {!Array<!goog.events.Listener>} listenerArray Array of listener.
+ * @param {!Function} listener The listener function.
+ * @param {boolean=} opt_useCapture The capture flag for the listener.
+ * @param {Object=} opt_listenerScope The listener scope.
+ * @return {number} The index of the matching listener within the
+ *     listenerArray.
+ * @private
+ */
+goog.events.ListenerMap.findListenerIndex_ = function(
+    listenerArray, listener, opt_useCapture, opt_listenerScope) {
+  for (var i = 0; i < listenerArray.length; ++i) {
+    var listenerObj = listenerArray[i];
+    if (!listenerObj.removed && listenerObj.listener == listener &&
+        listenerObj.capture == !!opt_useCapture &&
+        listenerObj.handler == opt_listenerScope) {
+      return i;
+    }
+  }
+  return -1;
+};
diff --git a/third_party/ink/closure/events/wheelevent.js b/third_party/ink/closure/events/wheelevent.js
new file mode 100644
index 0000000..770724a
--- /dev/null
+++ b/third_party/ink/closure/events/wheelevent.js
@@ -0,0 +1,170 @@
+// Copyright 2014 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 This class aims to smooth out inconsistencies between browser
+ * handling of wheel events by providing an event that is similar to that
+ * defined in the standard, but also easier to consume.
+ *
+ * It is based upon the WheelEvent, which allows for up to 3 dimensional
+ * scrolling events that come in units of either pixels, lines or pages.
+ * http://www.w3.org/TR/2014/WD-DOM-Level-3-Events-20140925/#interface-WheelEvent
+ *
+ * The significant difference here is that it also provides reasonable pixel
+ * deltas for clients that do not want to treat line and page scrolling events
+ * specially.
+ *
+ * Clients of this code should be aware that some input devices only fire a few
+ * discrete events (such as a mouse wheel without acceleration) whereas some can
+ * generate a large number of events for a single interaction (such as a
+ * touchpad with acceleration). There is no signal in the events to reliably
+ * distinguish between these.
+ *
+ * @author joshuawilder@google.com (Joshua Wilder)
+ * @see ../demos/wheelhandler.html
+ */
+
+goog.provide('goog.events.WheelEvent');
+
+goog.require('goog.asserts');
+goog.require('goog.events.BrowserEvent');
+
+
+
+/**
+ * A common class for wheel events. This is used with the WheelHandler.
+ *
+ * @param {Event} browserEvent Browser event object.
+ * @param {goog.events.WheelEvent.DeltaMode} deltaMode The delta mode units of
+ *     the wheel event.
+ * @param {number} deltaX The number of delta units the user in the X axis.
+ * @param {number} deltaY The number of delta units the user in the Y axis.
+ * @param {number} deltaZ The number of delta units the user in the Z axis.
+ * @constructor
+ * @extends {goog.events.BrowserEvent}
+ * @final
+ */
+goog.events.WheelEvent = function(
+    browserEvent, deltaMode, deltaX, deltaY, deltaZ) {
+  goog.events.WheelEvent.base(this, 'constructor', browserEvent);
+  goog.asserts.assert(browserEvent, 'Expecting a non-null browserEvent');
+
+  /** @type {goog.events.WheelEvent.EventType} */
+  this.type = goog.events.WheelEvent.EventType.WHEEL;
+
+  /**
+   * An enum corresponding to the units of this event.
+   * @type {goog.events.WheelEvent.DeltaMode}
+   */
+  this.deltaMode = deltaMode;
+
+  /**
+   * The number of delta units in the X axis.
+   * @type {number}
+   */
+  this.deltaX = deltaX;
+
+  /**
+   * The number of delta units in the Y axis.
+   * @type {number}
+   */
+  this.deltaY = deltaY;
+
+  /**
+   * The number of delta units in the Z axis.
+   * @type {number}
+   */
+  this.deltaZ = deltaZ;
+
+  // Ratio between delta and pixel values.
+  var pixelRatio = 1;  // Value for DeltaMode.PIXEL
+  switch (deltaMode) {
+    case goog.events.WheelEvent.DeltaMode.PAGE:
+      pixelRatio *= goog.events.WheelEvent.PIXELS_PER_PAGE_;
+      break;
+    case goog.events.WheelEvent.DeltaMode.LINE:
+      pixelRatio *= goog.events.WheelEvent.PIXELS_PER_LINE_;
+      break;
+  }
+
+  /**
+   * The number of delta pixels in the X axis. Code that doesn't want to handle
+   * different deltaMode units can just look here.
+   * @type {number}
+   */
+  this.pixelDeltaX = this.deltaX * pixelRatio;
+
+  /**
+   * The number of pixels in the Y axis. Code that doesn't want to
+   * handle different deltaMode units can just look here.
+   * @type {number}
+   */
+  this.pixelDeltaY = this.deltaY * pixelRatio;
+
+  /**
+   * The number of pixels scrolled in the Z axis. Code that doesn't want to
+   * handle different deltaMode units can just look here.
+   * @type {number}
+   */
+  this.pixelDeltaZ = this.deltaZ * pixelRatio;
+};
+goog.inherits(goog.events.WheelEvent, goog.events.BrowserEvent);
+
+
+/**
+ * Enum type for the events fired by the wheel handler.
+ * @enum {string}
+ */
+goog.events.WheelEvent.EventType = {
+  /** The user has provided wheel-based input. */
+  WHEEL: 'wheel'
+};
+
+
+/**
+ * Units for the deltas in a WheelEvent.
+ * @enum {number}
+ */
+goog.events.WheelEvent.DeltaMode = {
+  /** The units are in pixels. From DOM_DELTA_PIXEL. */
+  PIXEL: 0,
+  /** The units are in lines. From DOM_DELTA_LINE. */
+  LINE: 1,
+  /** The units are in pages. From DOM_DELTA_PAGE. */
+  PAGE: 2
+};
+
+
+/**
+ * A conversion number between line scroll units and pixel scroll units. The
+ * actual value per line can vary a lot between devices and font sizes. This
+ * number can not be perfect, but it should be reasonable for converting lines
+ * scroll events into pixels.
+ * @const {number}
+ * @private
+ */
+goog.events.WheelEvent.PIXELS_PER_LINE_ = 15;
+
+
+/**
+ * A conversion number between page scroll units and pixel scroll units. The
+ * actual value per page can vary a lot as many different devices have different
+ * screen sizes, and the window might not be taking up the full screen. This
+ * number can not be perfect, but it should be reasonable for converting page
+ * scroll events into pixels.
+ * @const {number}
+ * @private
+ */
+goog.events.WheelEvent.PIXELS_PER_PAGE_ =
+    30 * goog.events.WheelEvent.PIXELS_PER_LINE_;
diff --git a/third_party/ink/closure/format/format.js b/third_party/ink/closure/format/format.js
new file mode 100644
index 0000000..b938905
--- /dev/null
+++ b/third_party/ink/closure/format/format.js
@@ -0,0 +1,503 @@
+// Copyright 2006 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 Provides utility functions for formatting strings, numbers etc.
+ *
+ * @author pupius@google.com (Daniel Pupius)
+ */
+
+goog.provide('goog.format');
+
+goog.require('goog.i18n.GraphemeBreak');
+goog.require('goog.string');
+goog.require('goog.userAgent');
+
+
+/**
+ * Formats a number of bytes in human readable form.
+ * 54, 450K, 1.3M, 5G etc.
+ * @param {number} bytes The number of bytes to show.
+ * @param {number=} opt_decimals The number of decimals to use.  Defaults to 2.
+ * @return {string} The human readable form of the byte size.
+ */
+goog.format.fileSize = function(bytes, opt_decimals) {
+  return goog.format.numBytesToString(bytes, opt_decimals, false);
+};
+
+
+/**
+ * Checks whether string value containing scaling units (K, M, G, T, P, m,
+ * u, n) can be converted to a number.
+ *
+ * Where there is a decimal, there must be a digit to the left of the
+ * decimal point.
+ *
+ * Negative numbers are valid.
+ *
+ * Examples:
+ *   0, 1, 1.0, 10.4K, 2.3M, -0.3P, 1.2m
+ *
+ * @param {string} val String value to check.
+ * @return {boolean} True if string could be converted to a numeric value.
+ */
+goog.format.isConvertableScaledNumber = function(val) {
+  return goog.format.SCALED_NUMERIC_RE_.test(val);
+};
+
+
+/**
+ * Converts a string to numeric value, taking into account the units.
+ * If string ends in 'B', use binary conversion.
+ * @param {string} stringValue String to be converted to numeric value.
+ * @return {number} Numeric value for string.
+ */
+goog.format.stringToNumericValue = function(stringValue) {
+  if (goog.string.endsWith(stringValue, 'B')) {
+    return goog.format.stringToNumericValue_(
+        stringValue, goog.format.NUMERIC_SCALES_BINARY_);
+  }
+  return goog.format.stringToNumericValue_(
+      stringValue, goog.format.NUMERIC_SCALES_SI_);
+};
+
+
+/**
+ * Converts a string to number of bytes, taking into account the units.
+ * Binary conversion.
+ * @param {string} stringValue String to be converted to numeric value.
+ * @return {number} Numeric value for string.
+ */
+goog.format.stringToNumBytes = function(stringValue) {
+  return goog.format.stringToNumericValue_(
+      stringValue, goog.format.NUMERIC_SCALES_BINARY_);
+};
+
+
+/**
+ * Converts a numeric value to string representation. SI conversion.
+ * @param {number} val Value to be converted.
+ * @param {number=} opt_decimals The number of decimals to use.  Defaults to 2.
+ * @return {string} String representation of number.
+ */
+goog.format.numericValueToString = function(val, opt_decimals) {
+  return goog.format.numericValueToString_(
+      val, goog.format.NUMERIC_SCALES_SI_, opt_decimals);
+};
+
+
+/**
+ * Converts number of bytes to string representation. Binary conversion.
+ * Default is to return the additional 'B' suffix only for scales greater than
+ * 1K, e.g. '10.5KB' to minimize confusion with counts that are scaled by powers
+ * of 1000. Otherwise, suffix is empty string.
+ * @param {number} val Value to be converted.
+ * @param {number=} opt_decimals The number of decimals to use.  Defaults to 2.
+ * @param {boolean=} opt_suffix If true, include trailing 'B' in returned
+ *     string.  Default is true.
+ * @param {boolean=} opt_useSeparator If true, number and scale will be
+ *     separated by a no break space. Default is false.
+ * @return {string} String representation of number of bytes.
+ */
+goog.format.numBytesToString = function(
+    val, opt_decimals, opt_suffix, opt_useSeparator) {
+  var suffix = '';
+  if (!goog.isDef(opt_suffix) || opt_suffix) {
+    suffix = 'B';
+  }
+  return goog.format.numericValueToString_(
+      val, goog.format.NUMERIC_SCALES_BINARY_, opt_decimals, suffix,
+      opt_useSeparator);
+};
+
+
+/**
+ * Converts a string to numeric value, taking into account the units.
+ * @param {string} stringValue String to be converted to numeric value.
+ * @param {Object} conversion Dictionary of conversion scales.
+ * @return {number} Numeric value for string.  If it cannot be converted,
+ *    returns NaN.
+ * @private
+ */
+goog.format.stringToNumericValue_ = function(stringValue, conversion) {
+  var match = stringValue.match(goog.format.SCALED_NUMERIC_RE_);
+  if (!match) {
+    return NaN;
+  }
+  var val = Number(match[1]) * conversion[match[2]];
+  return val;
+};
+
+
+/**
+ * Converts a numeric value to string, using specified conversion
+ * scales.
+ * @param {number} val Value to be converted.
+ * @param {Object} conversion Dictionary of scaling factors.
+ * @param {number=} opt_decimals The number of decimals to use.  Default is 2.
+ * @param {string=} opt_suffix Optional suffix to append.
+ * @param {boolean=} opt_useSeparator If true, number and scale will be
+ *     separated by a space. Default is false.
+ * @return {string} The human readable form of the byte size.
+ * @private
+ */
+goog.format.numericValueToString_ = function(
+    val, conversion, opt_decimals, opt_suffix, opt_useSeparator) {
+  var prefixes = goog.format.NUMERIC_SCALE_PREFIXES_;
+  var orig_val = val;
+  var symbol = '';
+  var separator = '';
+  var scale = 1;
+  if (val < 0) {
+    val = -val;
+  }
+  for (var i = 0; i < prefixes.length; i++) {
+    var unit = prefixes[i];
+    scale = conversion[unit];
+    if (val >= scale || (scale <= 1 && val > 0.1 * scale)) {
+      // Treat values less than 1 differently, allowing 0.5 to be "0.5" rather
+      // than "500m"
+      symbol = unit;
+      break;
+    }
+  }
+  if (!symbol) {
+    scale = 1;
+  } else {
+    if (opt_suffix) {
+      symbol += opt_suffix;
+    }
+    if (opt_useSeparator) {
+      separator = ' ';
+    }
+  }
+  var ex = Math.pow(10, goog.isDef(opt_decimals) ? opt_decimals : 2);
+  return Math.round(orig_val / scale * ex) / ex + separator + symbol;
+};
+
+
+/**
+ * Regular expression for detecting scaling units, such as K, M, G, etc. for
+ * converting a string representation to a numeric value.
+ *
+ * Also allow 'k' to be aliased to 'K'.  These could be used for SI (powers
+ * of 1000) or Binary (powers of 1024) conversions.
+ *
+ * Also allow final 'B' to be interpreted as byte-count, implicitly triggering
+ * binary conversion (e.g., '10.2MB').
+ *
+ * @type {RegExp}
+ * @private
+ */
+goog.format.SCALED_NUMERIC_RE_ =
+    /^([-]?\d+\.?\d*)([K,M,G,T,P,E,Z,Y,k,m,u,n]?)[B]?$/;
+
+
+/**
+ * Ordered list of scaling prefixes in decreasing order.
+ * @private {Array<string>}
+ */
+goog.format.NUMERIC_SCALE_PREFIXES_ =
+    ['Y', 'Z', 'E', 'P', 'T', 'G', 'M', 'K', '', 'm', 'u', 'n'];
+
+
+/**
+ * Scaling factors for conversion of numeric value to string.  SI conversion.
+ * @type {Object}
+ * @private
+ */
+goog.format.NUMERIC_SCALES_SI_ = {
+  '': 1,
+  'n': 1e-9,
+  'u': 1e-6,
+  'm': 1e-3,
+  'k': 1e3,
+  'K': 1e3,
+  'M': 1e6,
+  'G': 1e9,
+  'T': 1e12,
+  'P': 1e15,
+  'E': 1e18,
+  'Z': 1e21,
+  'Y': 1e24
+};
+
+
+/**
+ * Scaling factors for conversion of numeric value to string.  Binary
+ * conversion.
+ * @type {Object}
+ * @private
+ */
+goog.format.NUMERIC_SCALES_BINARY_ = {
+  '': 1,
+  'n': Math.pow(1024, -3),
+  'u': Math.pow(1024, -2),
+  'm': 1.0 / 1024,
+  'k': 1024,
+  'K': 1024,
+  'M': Math.pow(1024, 2),
+  'G': Math.pow(1024, 3),
+  'T': Math.pow(1024, 4),
+  'P': Math.pow(1024, 5),
+  'E': Math.pow(1024, 6),
+  'Z': Math.pow(1024, 7),
+  'Y': Math.pow(1024, 8)
+};
+
+
+/**
+ * First Unicode code point that has the Mark property.
+ * @type {number}
+ * @private
+ */
+goog.format.FIRST_GRAPHEME_EXTEND_ = 0x300;
+
+
+/**
+ * Returns true if and only if given character should be treated as a breaking
+ * space. All ASCII control characters, the main Unicode range of spacing
+ * characters (U+2000 to U+200B inclusive except for U+2007), and several other
+ * Unicode space characters are treated as breaking spaces.
+ * @param {number} charCode The character code under consideration.
+ * @return {boolean} True if the character is a breaking space.
+ * @private
+ */
+goog.format.isTreatedAsBreakingSpace_ = function(charCode) {
+  return (charCode <= goog.format.WbrToken_.SPACE) ||
+      (charCode >= 0x1000 &&
+       ((charCode >= 0x2000 && charCode <= 0x2006) ||
+        (charCode >= 0x2008 && charCode <= 0x200B) || charCode == 0x1680 ||
+        charCode == 0x180E || charCode == 0x2028 || charCode == 0x2029 ||
+        charCode == 0x205f || charCode == 0x3000));
+};
+
+
+/**
+ * Returns true if and only if given character is an invisible formatting
+ * character.
+ * @param {number} charCode The character code under consideration.
+ * @return {boolean} True if the character is an invisible formatting character.
+ * @private
+ */
+goog.format.isInvisibleFormattingCharacter_ = function(charCode) {
+  // See: http://unicode.org/charts/PDF/U2000.pdf
+  return (charCode >= 0x200C && charCode <= 0x200F) ||
+      (charCode >= 0x202A && charCode <= 0x202E);
+};
+
+
+/**
+ * Inserts word breaks into an HTML string at a given interval.  The counter is
+ * reset if a space or a character which behaves like a space is encountered,
+ * but it isn't incremented if an invisible formatting character is encountered.
+ * WBRs aren't inserted into HTML tags or entities.  Entities count towards the
+ * character count, HTML tags do not.
+ *
+ * With common strings aliased, objects allocations are constant based on the
+ * length of the string: N + 3. This guarantee does not hold if the string
+ * contains an element >= U+0300 and hasGraphemeBreak is non-trivial.
+ *
+ * @param {string} str HTML to insert word breaks into.
+ * @param {function(number, number, boolean): boolean} hasGraphemeBreak A
+ *     function determining if there is a grapheme break between two characters,
+ *     in the same signature as goog.i18n.GraphemeBreak.hasGraphemeBreak.
+ * @param {number=} opt_maxlen Maximum length after which to ensure
+ *     there is a break.  Default is 10 characters.
+ * @return {string} The string including word breaks.
+ * @private
+ */
+goog.format.insertWordBreaksGeneric_ = function(
+    str, hasGraphemeBreak, opt_maxlen) {
+  var maxlen = opt_maxlen || 10;
+  if (maxlen > str.length) return str;
+
+  var rv = [];
+  var n = 0;  // The length of the current token
+
+  // This will contain the ampersand or less-than character if one of the
+  // two has been seen; otherwise, the value is zero.
+  var nestingCharCode = 0;
+
+  // First character position from input string that has not been outputted.
+  var lastDumpPosition = 0;
+
+  var charCode = 0;
+  for (var i = 0; i < str.length; i++) {
+    // Using charCodeAt versus charAt avoids allocating new string objects.
+    var lastCharCode = charCode;
+    charCode = str.charCodeAt(i);
+
+    // Don't add a WBR before characters that might be grapheme extending.
+    var isPotentiallyGraphemeExtending =
+        charCode >= goog.format.FIRST_GRAPHEME_EXTEND_ &&
+        !hasGraphemeBreak(lastCharCode, charCode, true);
+
+    // Don't add a WBR at the end of a word. For the purposes of determining
+    // work breaks, all ASCII control characters and some commonly encountered
+    // Unicode spacing characters are treated as breaking spaces.
+    if (n >= maxlen && !goog.format.isTreatedAsBreakingSpace_(charCode) &&
+        !isPotentiallyGraphemeExtending) {
+      // Flush everything seen so far, and append a word break.
+      rv.push(str.substring(lastDumpPosition, i), goog.format.WORD_BREAK_HTML);
+      lastDumpPosition = i;
+      n = 0;
+    }
+
+    if (!nestingCharCode) {
+      // Not currently within an HTML tag or entity
+
+      if (charCode == goog.format.WbrToken_.LT ||
+          charCode == goog.format.WbrToken_.AMP) {
+        // Entering an HTML Entity '&' or open tag '<'
+        nestingCharCode = charCode;
+      } else if (goog.format.isTreatedAsBreakingSpace_(charCode)) {
+        // A space or control character -- reset the token length
+        n = 0;
+      } else if (!goog.format.isInvisibleFormattingCharacter_(charCode)) {
+        // A normal flow character - increment.  For grapheme extending
+        // characters, this is not *technically* a new character.  However,
+        // since the grapheme break detector might be overly conservative,
+        // we have to continue incrementing, or else we won't even be able
+        // to add breaks when we get to things like punctuation.  For the
+        // case where we have a full grapheme break detector, it is okay if
+        // we occasionally break slightly early.
+        n++;
+      }
+    } else if (
+        charCode == goog.format.WbrToken_.GT &&
+        nestingCharCode == goog.format.WbrToken_.LT) {
+      // Leaving an HTML tag, treat the tag as zero-length
+      nestingCharCode = 0;
+    } else if (
+        charCode == goog.format.WbrToken_.SEMI_COLON &&
+        nestingCharCode == goog.format.WbrToken_.AMP) {
+      // Leaving an HTML entity, treat it as length one
+      nestingCharCode = 0;
+      n++;
+    }
+  }
+
+  // Take care of anything we haven't flushed so far.
+  rv.push(str.substr(lastDumpPosition));
+
+  return rv.join('');
+};
+
+
+/**
+ * Inserts word breaks into an HTML string at a given interval.
+ *
+ * This method is as aggressive as possible, using a full table of Unicode
+ * characters where it is legal to insert word breaks; however, this table
+ * comes at a 2.5k pre-gzip (~1k post-gzip) size cost.  Consider using
+ * insertWordBreaksBasic to minimize the size impact.
+ *
+ * @param {string} str HTML to insert word breaks into.
+ * @param {number=} opt_maxlen Maximum length after which to ensure there is a
+ *     break.  Default is 10 characters.
+ * @return {string} The string including word breaks.
+ * @deprecated Prefer wrapping with CSS word-wrap: break-word.
+ */
+goog.format.insertWordBreaks = function(str, opt_maxlen) {
+  return goog.format.insertWordBreaksGeneric_(
+      str, goog.i18n.GraphemeBreak.hasGraphemeBreak, opt_maxlen);
+};
+
+
+/**
+ * Determines conservatively if a character has a Grapheme break.
+ *
+ * Conforms to a similar signature as goog.i18n.GraphemeBreak, but is overly
+ * conservative, returning true only for characters in common scripts that
+ * are simple to account for.
+ *
+ * @param {number} lastCharCode The previous character code.  Ignored.
+ * @param {number} charCode The character code under consideration.  It must be
+ *     at least \u0300 as a precondition -- this case is covered by
+ *     insertWordBreaksGeneric_.
+ * @param {boolean=} opt_extended Ignored, to conform with the interface.
+ * @return {boolean} Whether it is one of the recognized subsets of characters
+ *     with a grapheme break.
+ * @private
+ */
+goog.format.conservativelyHasGraphemeBreak_ = function(
+    lastCharCode, charCode, opt_extended) {
+  // Return false for everything except the most common Cyrillic characters.
+  // Don't worry about Latin characters, because insertWordBreaksGeneric_
+  // itself already handles those.
+  // TODO(gboyer): Also account for Greek, Armenian, and Georgian if it is
+  // simple to do so.
+  return charCode >= 0x400 && charCode < 0x523;
+};
+
+
+// TODO(gboyer): Consider using a compile-time flag to switch implementations
+// rather than relying on the developers to toggle implementations.
+/**
+ * Inserts word breaks into an HTML string at a given interval.
+ *
+ * This method is less aggressive than insertWordBreaks, only inserting
+ * breaks next to punctuation and between Latin or Cyrillic characters.
+ * However, this is good enough for the common case of URLs.  It also
+ * works for all Latin and Cyrillic languages, plus CJK has no need for word
+ * breaks.  When this method is used, goog.i18n.GraphemeBreak may be dead
+ * code eliminated.
+ *
+ * @param {string} str HTML to insert word breaks into.
+ * @param {number=} opt_maxlen Maximum length after which to ensure there is a
+ *     break.  Default is 10 characters.
+ * @return {string} The string including word breaks.
+ * @deprecated Prefer wrapping with CSS word-wrap: break-word.
+ */
+goog.format.insertWordBreaksBasic = function(str, opt_maxlen) {
+  return goog.format.insertWordBreaksGeneric_(
+      str, goog.format.conservativelyHasGraphemeBreak_, opt_maxlen);
+};
+
+
+/**
+ * True iff the current userAgent is IE8 or above.
+ * @type {boolean}
+ * @private
+ */
+goog.format.IS_IE8_OR_ABOVE_ =
+    goog.userAgent.IE && goog.userAgent.isVersionOrHigher(8);
+
+
+/**
+ * Constant for the WBR replacement used by insertWordBreaks.  Safari requires
+ * <wbr></wbr>, Opera needs the &shy; entity, though this will give a visible
+ * hyphen at breaks.  IE8 uses a zero width space.
+ * Other browsers just use <wbr>.
+ * @type {string}
+ */
+goog.format.WORD_BREAK_HTML =
+    goog.userAgent.WEBKIT ? '<wbr></wbr>' : goog.userAgent.OPERA ?
+                            '&shy;' :
+                            goog.format.IS_IE8_OR_ABOVE_ ? '&#8203;' : '<wbr>';
+
+
+/**
+ * Tokens used within insertWordBreaks.
+ * @private
+ * @enum {number}
+ */
+goog.format.WbrToken_ = {
+  LT: 60,          // '<'.charCodeAt(0)
+  GT: 62,          // '>'.charCodeAt(0)
+  AMP: 38,         // '&'.charCodeAt(0)
+  SEMI_COLON: 59,  // ';'.charCodeAt(0)
+  SPACE: 32        // ' '.charCodeAt(0)
+};
diff --git a/third_party/ink/closure/fs/url.js b/third_party/ink/closure/fs/url.js
new file mode 100644
index 0000000..1204908
--- /dev/null
+++ b/third_party/ink/closure/fs/url.js
@@ -0,0 +1,106 @@
+// Copyright 2015 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 Wrapper for URL and its createObjectUrl and revokeObjectUrl
+ * methods that are part of the HTML5 File API.
+ */
+
+goog.provide('goog.fs.url');
+
+
+/**
+ * Creates a blob URL for a blob object.
+ * Throws an error if the browser does not support Object Urls.
+ *
+ * @param {!Blob} blob The object for which to create the URL.
+ * @return {string} The URL for the object.
+ */
+goog.fs.url.createObjectUrl = function(blob) {
+  return goog.fs.url.getUrlObject_().createObjectURL(blob);
+};
+
+
+/**
+ * Revokes a URL created by {@link goog.fs.url.createObjectUrl}.
+ * Throws an error if the browser does not support Object Urls.
+ *
+ * @param {string} url The URL to revoke.
+ */
+goog.fs.url.revokeObjectUrl = function(url) {
+  goog.fs.url.getUrlObject_().revokeObjectURL(url);
+};
+
+
+/**
+ * @typedef {{createObjectURL: (function(!Blob): string),
+ *            revokeObjectURL: function(string): void}}
+ */
+goog.fs.url.UrlObject_;
+
+
+/**
+ * Get the object that has the createObjectURL and revokeObjectURL functions for
+ * this browser.
+ *
+ * @return {goog.fs.url.UrlObject_} The object for this browser.
+ * @private
+ */
+goog.fs.url.getUrlObject_ = function() {
+  var urlObject = goog.fs.url.findUrlObject_();
+  if (urlObject != null) {
+    return urlObject;
+  } else {
+    throw new Error('This browser doesn\'t seem to support blob URLs');
+  }
+};
+
+
+/**
+ * Finds the object that has the createObjectURL and revokeObjectURL functions
+ * for this browser.
+ *
+ * @return {?goog.fs.url.UrlObject_} The object for this browser or null if the
+ *     browser does not support Object Urls.
+ * @private
+ */
+goog.fs.url.findUrlObject_ = function() {
+  // This is what the spec says to do
+  // http://dev.w3.org/2006/webapi/FileAPI/#dfn-createObjectURL
+  if (goog.isDef(goog.global.URL) &&
+      goog.isDef(goog.global.URL.createObjectURL)) {
+    return /** @type {goog.fs.url.UrlObject_} */ (goog.global.URL);
+    // This is what Chrome does (as of 10.0.648.6 dev)
+  } else if (
+      goog.isDef(goog.global.webkitURL) &&
+      goog.isDef(goog.global.webkitURL.createObjectURL)) {
+    return /** @type {goog.fs.url.UrlObject_} */ (goog.global.webkitURL);
+    // This is what the spec used to say to do
+  } else if (goog.isDef(goog.global.createObjectURL)) {
+    return /** @type {goog.fs.url.UrlObject_} */ (goog.global);
+  } else {
+    return null;
+  }
+};
+
+
+/**
+ * Checks whether this browser supports Object Urls. If not, calls to
+ * createObjectUrl and revokeObjectUrl will result in an error.
+ *
+ * @return {boolean} True if this browser supports Object Urls.
+ */
+goog.fs.url.browserSupportsObjectUrls = function() {
+  return goog.fs.url.findUrlObject_() != null;
+};
diff --git a/third_party/ink/closure/functions/functions.js b/third_party/ink/closure/functions/functions.js
new file mode 100644
index 0000000..f735888
--- /dev/null
+++ b/third_party/ink/closure/functions/functions.js
@@ -0,0 +1,485 @@
+// Copyright 2008 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 Utilities for creating functions. Loosely inspired by the
+ * java classes: http://goo.gl/GM0Hmu and http://goo.gl/6k7nI8.
+ *
+ * @author nicksantos@google.com (Nick Santos)
+ */
+
+
+goog.provide('goog.functions');
+
+
+/**
+ * Creates a function that always returns the same value.
+ * @param {T} retValue The value to return.
+ * @return {function():T} The new function.
+ * @template T
+ */
+goog.functions.constant = function(retValue) {
+  return function() { return retValue; };
+};
+
+
+/**
+ * Always returns false.
+ * @type {function(...): boolean}
+ */
+goog.functions.FALSE = goog.functions.constant(false);
+
+
+/**
+ * Always returns true.
+ * @type {function(...): boolean}
+ */
+goog.functions.TRUE = goog.functions.constant(true);
+
+
+/**
+ * Always returns NULL.
+ * @type {function(...): null}
+ */
+goog.functions.NULL = goog.functions.constant(null);
+
+
+/**
+ * A simple function that returns the first argument of whatever is passed
+ * into it.
+ * @param {T=} opt_returnValue The single value that will be returned.
+ * @param {...*} var_args Optional trailing arguments. These are ignored.
+ * @return {T} The first argument passed in, or undefined if nothing was passed.
+ * @template T
+ */
+goog.functions.identity = function(opt_returnValue, var_args) {
+  return opt_returnValue;
+};
+
+
+/**
+ * Creates a function that always throws an error with the given message.
+ * @param {string} message The error message.
+ * @return {!Function} The error-throwing function.
+ */
+goog.functions.error = function(message) {
+  return function() {
+    throw new Error(message);
+  };
+};
+
+
+/**
+ * Creates a function that throws the given object.
+ * @param {*} err An object to be thrown.
+ * @return {!Function} The error-throwing function.
+ */
+goog.functions.fail = function(err) {
+  return function() { throw err; };
+};
+
+
+/**
+ * Given a function, create a function that keeps opt_numArgs arguments and
+ * silently discards all additional arguments.
+ * @param {Function} f The original function.
+ * @param {number=} opt_numArgs The number of arguments to keep. Defaults to 0.
+ * @return {!Function} A version of f that only keeps the first opt_numArgs
+ *     arguments.
+ */
+goog.functions.lock = function(f, opt_numArgs) {
+  opt_numArgs = opt_numArgs || 0;
+  return function() {
+    return f.apply(this, Array.prototype.slice.call(arguments, 0, opt_numArgs));
+  };
+};
+
+
+/**
+ * Creates a function that returns its nth argument.
+ * @param {number} n The position of the return argument.
+ * @return {!Function} A new function.
+ */
+goog.functions.nth = function(n) {
+  return function() { return arguments[n]; };
+};
+
+
+/**
+ * Like goog.partial(), except that arguments are added after arguments to the
+ * returned function.
+ *
+ * Usage:
+ * function f(arg1, arg2, arg3, arg4) { ... }
+ * var g = goog.functions.partialRight(f, arg3, arg4);
+ * g(arg1, arg2);
+ *
+ * @param {!Function} fn A function to partially apply.
+ * @param {...*} var_args Additional arguments that are partially applied to fn
+ *     at the end.
+ * @return {!Function} A partially-applied form of the function goog.partial()
+ *     was invoked as a method of.
+ */
+goog.functions.partialRight = function(fn, var_args) {
+  var rightArgs = Array.prototype.slice.call(arguments, 1);
+  return function() {
+    var newArgs = Array.prototype.slice.call(arguments);
+    newArgs.push.apply(newArgs, rightArgs);
+    return fn.apply(this, newArgs);
+  };
+};
+
+
+/**
+ * Given a function, create a new function that swallows its return value
+ * and replaces it with a new one.
+ * @param {Function} f A function.
+ * @param {T} retValue A new return value.
+ * @return {function(...?):T} A new function.
+ * @template T
+ */
+goog.functions.withReturnValue = function(f, retValue) {
+  return goog.functions.sequence(f, goog.functions.constant(retValue));
+};
+
+
+/**
+ * Creates a function that returns whether its argument equals the given value.
+ *
+ * Example:
+ * var key = goog.object.findKey(obj, goog.functions.equalTo('needle'));
+ *
+ * @param {*} value The value to compare to.
+ * @param {boolean=} opt_useLooseComparison Whether to use a loose (==)
+ *     comparison rather than a strict (===) one. Defaults to false.
+ * @return {function(*):boolean} The new function.
+ */
+goog.functions.equalTo = function(value, opt_useLooseComparison) {
+  return function(other) {
+    return opt_useLooseComparison ? (value == other) : (value === other);
+  };
+};
+
+
+/**
+ * Creates the composition of the functions passed in.
+ * For example, (goog.functions.compose(f, g))(a) is equivalent to f(g(a)).
+ * @param {function(...?):T} fn The final function.
+ * @param {...Function} var_args A list of functions.
+ * @return {function(...?):T} The composition of all inputs.
+ * @template T
+ */
+goog.functions.compose = function(fn, var_args) {
+  var functions = arguments;
+  var length = functions.length;
+  return function() {
+    var result;
+    if (length) {
+      result = functions[length - 1].apply(this, arguments);
+    }
+
+    for (var i = length - 2; i >= 0; i--) {
+      result = functions[i].call(this, result);
+    }
+    return result;
+  };
+};
+
+
+/**
+ * Creates a function that calls the functions passed in in sequence, and
+ * returns the value of the last function. For example,
+ * (goog.functions.sequence(f, g))(x) is equivalent to f(x),g(x).
+ * @param {...Function} var_args A list of functions.
+ * @return {!Function} A function that calls all inputs in sequence.
+ */
+goog.functions.sequence = function(var_args) {
+  var functions = arguments;
+  var length = functions.length;
+  return function() {
+    var result;
+    for (var i = 0; i < length; i++) {
+      result = functions[i].apply(this, arguments);
+    }
+    return result;
+  };
+};
+
+
+/**
+ * Creates a function that returns true if each of its components evaluates
+ * to true. The components are evaluated in order, and the evaluation will be
+ * short-circuited as soon as a function returns false.
+ * For example, (goog.functions.and(f, g))(x) is equivalent to f(x) && g(x).
+ * @param {...Function} var_args A list of functions.
+ * @return {function(...?):boolean} A function that ANDs its component
+ *      functions.
+ */
+goog.functions.and = function(var_args) {
+  var functions = arguments;
+  var length = functions.length;
+  return function() {
+    for (var i = 0; i < length; i++) {
+      if (!functions[i].apply(this, arguments)) {
+        return false;
+      }
+    }
+    return true;
+  };
+};
+
+
+/**
+ * Creates a function that returns true if any of its components evaluates
+ * to true. The components are evaluated in order, and the evaluation will be
+ * short-circuited as soon as a function returns true.
+ * For example, (goog.functions.or(f, g))(x) is equivalent to f(x) || g(x).
+ * @param {...Function} var_args A list of functions.
+ * @return {function(...?):boolean} A function that ORs its component
+ *    functions.
+ */
+goog.functions.or = function(var_args) {
+  var functions = arguments;
+  var length = functions.length;
+  return function() {
+    for (var i = 0; i < length; i++) {
+      if (functions[i].apply(this, arguments)) {
+        return true;
+      }
+    }
+    return false;
+  };
+};
+
+
+/**
+ * Creates a function that returns the Boolean opposite of a provided function.
+ * For example, (goog.functions.not(f))(x) is equivalent to !f(x).
+ * @param {!Function} f The original function.
+ * @return {function(...?):boolean} A function that delegates to f and returns
+ * opposite.
+ */
+goog.functions.not = function(f) {
+  return function() { return !f.apply(this, arguments); };
+};
+
+
+/**
+ * Generic factory function to construct an object given the constructor
+ * and the arguments. Intended to be bound to create object factories.
+ *
+ * Example:
+ *
+ * var factory = goog.partial(goog.functions.create, Class);
+ *
+ * @param {function(new:T, ...)} constructor The constructor for the Object.
+ * @param {...*} var_args The arguments to be passed to the constructor.
+ * @return {T} A new instance of the class given in {@code constructor}.
+ * @template T
+ */
+goog.functions.create = function(constructor, var_args) {
+  /**
+   * @constructor
+   * @final
+   */
+  var temp = function() {};
+  temp.prototype = constructor.prototype;
+
+  // obj will have constructor's prototype in its chain and
+  // 'obj instanceof constructor' will be true.
+  var obj = new temp();
+
+  // obj is initialized by constructor.
+  // arguments is only array-like so lacks shift(), but can be used with
+  // the Array prototype function.
+  constructor.apply(obj, Array.prototype.slice.call(arguments, 1));
+  return obj;
+};
+
+
+/**
+ * @define {boolean} Whether the return value cache should be used.
+ *    This should only be used to disable caches when testing.
+ */
+goog.define('goog.functions.CACHE_RETURN_VALUE', true);
+
+
+/**
+ * Gives a wrapper function that caches the return value of a parameterless
+ * function when first called.
+ *
+ * When called for the first time, the given function is called and its
+ * return value is cached (thus this is only appropriate for idempotent
+ * functions).  Subsequent calls will return the cached return value. This
+ * allows the evaluation of expensive functions to be delayed until first used.
+ *
+ * To cache the return values of functions with parameters, see goog.memoize.
+ *
+ * @param {function():T} fn A function to lazily evaluate.
+ * @return {function():T} A wrapped version the function.
+ * @template T
+ */
+goog.functions.cacheReturnValue = function(fn) {
+  var called = false;
+  var value;
+
+  return function() {
+    if (!goog.functions.CACHE_RETURN_VALUE) {
+      return fn();
+    }
+
+    if (!called) {
+      value = fn();
+      called = true;
+    }
+
+    return value;
+  };
+};
+
+
+/**
+ * Wraps a function to allow it to be called, at most, once. All
+ * additional calls are no-ops.
+ *
+ * This is particularly useful for initialization functions
+ * that should be called, at most, once.
+ *
+ * @param {function():*} f Function to call.
+ * @return {function():undefined} Wrapped function.
+ */
+goog.functions.once = function(f) {
+  // Keep a reference to the function that we null out when we're done with
+  // it -- that way, the function can be GC'd when we're done with it.
+  var inner = f;
+  return function() {
+    if (inner) {
+      var tmp = inner;
+      inner = null;
+      tmp();
+    }
+  };
+};
+
+
+/**
+ * Wraps a function to allow it to be called, at most, once per interval
+ * (specified in milliseconds). If the wrapper function is called N times within
+ * that interval, only the Nth call will go through.
+ *
+ * This is particularly useful for batching up repeated actions where the
+ * last action should win. This can be used, for example, for refreshing an
+ * autocomplete pop-up every so often rather than updating with every keystroke,
+ * since the final text typed by the user is the one that should produce the
+ * final autocomplete results. For more stateful debouncing with support for
+ * pausing, resuming, and canceling debounced actions, use {@code
+ * goog.async.Debouncer}.
+ *
+ * @param {function(this:SCOPE, ...?)} f Function to call.
+ * @param {number} interval Interval over which to debounce. The function will
+ *     only be called after the full interval has elapsed since the last call.
+ * @param {SCOPE=} opt_scope Object in whose scope to call the function.
+ * @return {function(...?): undefined} Wrapped function.
+ * @template SCOPE
+ */
+goog.functions.debounce = function(f, interval, opt_scope) {
+  var timeout = 0;
+  return /** @type {function(...?)} */ (function(var_args) {
+    goog.global.clearTimeout(timeout);
+    var args = arguments;
+    timeout = goog.global.setTimeout(function() {
+      f.apply(opt_scope, args);
+    }, interval);
+  });
+};
+
+
+/**
+ * Wraps a function to allow it to be called, at most, once per interval
+ * (specified in milliseconds). If the wrapper function is called N times in
+ * that interval, both the 1st and the Nth calls will go through.
+ *
+ * This is particularly useful for limiting repeated user requests where the
+ * the last action should win, but you also don't want to wait until the end of
+ * the interval before sending a request out, as it leads to a perception of
+ * slowness for the user.
+ *
+ * @param {function(this:SCOPE, ...?)} f Function to call.
+ * @param {number} interval Interval over which to throttle. The function can
+ *     only be called once per interval.
+ * @param {SCOPE=} opt_scope Object in whose scope to call the function.
+ * @return {function(...?): undefined} Wrapped function.
+ * @template SCOPE
+ */
+goog.functions.throttle = function(f, interval, opt_scope) {
+  var timeout = 0;
+  var shouldFire = false;
+  var args = [];
+
+  var handleTimeout = function() {
+    timeout = 0;
+    if (shouldFire) {
+      shouldFire = false;
+      fire();
+    }
+  };
+
+  var fire = function() {
+    timeout = goog.global.setTimeout(handleTimeout, interval);
+    f.apply(opt_scope, args);
+  };
+
+  return /** @type {function(...?)} */ (function(var_args) {
+    args = arguments;
+    if (!timeout) {
+      fire();
+    } else {
+      shouldFire = true;
+    }
+  });
+};
+
+
+/**
+ * Wraps a function to allow it to be called, at most, once per interval
+ * (specified in milliseconds). If the wrapper function is called N times within
+ * that interval, only the 1st call will go through.
+ *
+ * This is particularly useful for limiting repeated user requests where the
+ * first request is guaranteed to have all the data required to perform the
+ * final action, so there's no need to wait until the end of the interval before
+ * sending the request out.
+ *
+ * @param {function(this:SCOPE, ...?)} f Function to call.
+ * @param {number} interval Interval over which to rate-limit. The function will
+ *     only be called once per interval, and ignored for the remainer of the
+ *     interval.
+ * @param {SCOPE=} opt_scope Object in whose scope to call the function.
+ * @return {function(...?): undefined} Wrapped function.
+ * @template SCOPE
+ */
+goog.functions.rateLimit = function(f, interval, opt_scope) {
+  var timeout = 0;
+
+  var handleTimeout = function() {
+    timeout = 0;
+  };
+
+  return /** @type {function(...?)} */ (function(var_args) {
+    if (!timeout) {
+      timeout = goog.global.setTimeout(handleTimeout, interval);
+      f.apply(opt_scope, arguments);
+    }
+  });
+};
diff --git a/third_party/ink/closure/html/legacyconversions.js b/third_party/ink/closure/html/legacyconversions.js
new file mode 100644
index 0000000..0148977
--- /dev/null
+++ b/third_party/ink/closure/html/legacyconversions.js
@@ -0,0 +1,204 @@
+// Copyright 2013 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 Transitional utilities to unsafely trust random strings as
+ * goog.html types. Intended for temporary use when upgrading a library that
+ * used to accept plain strings to use safe types, but where it's not
+ * practical to transitively update callers.
+ *
+ * IMPORTANT: No new code should use the conversion functions in this file,
+ * they are intended for refactoring old code to use goog.html types. New code
+ * should construct goog.html types via their APIs, template systems or
+ * sanitizers. If that’s not possible it should use
+ * goog.html.uncheckedconversions and undergo security review.
+
+ * MOE:begin_intracomment_strip
+ * At Google goog.html.legacyconversions are restricted via both BUILD
+ * visibility and Conformance rules. The goal is to allow us to progressively
+ * get rid of using strings to represent HTML-related data which is passed to
+ * DOM APIs that execute script (like innerHTML or Anchor.href), while avoiding
+ * regressions. Please carefully read the documentation below before using
+ * these functions. If you have questions contact ise-hardening@ and we’ll
+ * gladly help.
+ * MOE:end_intracomment_strip
+ *
+ * The semantics of the conversions in goog.html.legacyconversions are very
+ * different from the ones provided by goog.html.uncheckedconversions. The
+ * latter are for use in code where it has been established through manual
+ * security review that the value produced by a piece of code will always
+ * satisfy the SafeHtml contract (e.g., the output of a secure HTML sanitizer).
+ * In uses of goog.html.legacyconversions, this guarantee is not given -- the
+ * value in question originates in unreviewed legacy code and there is no
+ * guarantee that it satisfies the SafeHtml contract.
+ *
+ * There are only three valid uses of legacyconversions:
+ *
+ * 1. Introducing a goog.html version of a function which currently consumes
+ * string and passes that string to a DOM API which can execute script - and
+ * hence cause XSS - like innerHTML. For example, Dialog might expose a
+ * setContent method which takes a string and sets the innerHTML property of
+ * an element with it. In this case a setSafeHtmlContent function could be
+ * added, consuming goog.html.SafeHtml instead of string, and using
+ * goog.dom.safe.setInnerHtml instead of directly setting innerHTML.
+ * setContent could then internally use legacyconversions to create a SafeHtml
+ * from string and pass the SafeHtml to setSafeHtmlContent. In this scenario
+ * remember to document the use of legacyconversions in the modified setContent
+ * and consider deprecating it as well.
+ *
+ * 2. Automated refactoring of application code which handles HTML as string
+ * but needs to call a function which only takes goog.html types. For example,
+ * in the Dialog scenario from (1) an alternative option would be to refactor
+ * setContent to accept goog.html.SafeHtml instead of string and then refactor
+ * all current callers to use legacyconversions to pass SafeHtml. This is
+ * generally preferable to (1) because it keeps the library clean of
+ * legacyconversions, and makes code sites in application code that are
+ * potentially vulnerable to XSS more apparent.
+ *
+ * 3. Old code which needs to call APIs which consume goog.html types and for
+ * which it is prohibitively expensive to refactor to use goog.html types.
+ * Generally, this is code where safety from XSS is either hopeless or
+ * unimportant.
+ *
+ * @visibility {//javascript/closure/html:approved_for_legacy_conversion}
+ * @visibility {//javascript/closure/bin/sizetests:__pkg__}
+ */
+
+
+goog.provide('goog.html.legacyconversions');
+
+goog.require('goog.html.SafeHtml');
+goog.require('goog.html.SafeScript');
+goog.require('goog.html.SafeStyle');
+goog.require('goog.html.SafeStyleSheet');
+goog.require('goog.html.SafeUrl');
+goog.require('goog.html.TrustedResourceUrl');
+
+
+/**
+ * Performs an "unchecked conversion" from string to SafeHtml for legacy API
+ * purposes.
+ *
+ * Please read fileoverview documentation before using.
+ *
+ * @param {string} html A string to be converted to SafeHtml.
+ * @return {!goog.html.SafeHtml} The value of html, wrapped in a SafeHtml
+ *     object.
+ */
+goog.html.legacyconversions.safeHtmlFromString = function(html) {
+  goog.html.legacyconversions.reportCallback_();
+  return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
+      html, null /* dir */);
+};
+
+
+/**
+ * Performs an "unchecked conversion" from string to SafeScript for legacy API
+ * purposes.
+ *
+ * Please read fileoverview documentation before using.
+ *
+ * @param {string} script A string to be converted to SafeScript.
+ * @return {!goog.html.SafeScript} The value of script, wrapped in a SafeScript
+ *     object.
+ */
+goog.html.legacyconversions.safeScriptFromString = function(script) {
+  goog.html.legacyconversions.reportCallback_();
+  return goog.html.SafeScript.createSafeScriptSecurityPrivateDoNotAccessOrElse(
+      script);
+};
+
+
+/**
+ * Performs an "unchecked conversion" from string to SafeStyle for legacy API
+ * purposes.
+ *
+ * Please read fileoverview documentation before using.
+ *
+ * @param {string} style A string to be converted to SafeStyle.
+ * @return {!goog.html.SafeStyle} The value of style, wrapped in a SafeStyle
+ *     object.
+ */
+goog.html.legacyconversions.safeStyleFromString = function(style) {
+  goog.html.legacyconversions.reportCallback_();
+  return goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(
+      style);
+};
+
+
+/**
+ * Performs an "unchecked conversion" from string to SafeStyleSheet for legacy
+ * API purposes.
+ *
+ * Please read fileoverview documentation before using.
+ *
+ * @param {string} styleSheet A string to be converted to SafeStyleSheet.
+ * @return {!goog.html.SafeStyleSheet} The value of style sheet, wrapped in
+ *     a SafeStyleSheet object.
+ */
+goog.html.legacyconversions.safeStyleSheetFromString = function(styleSheet) {
+  goog.html.legacyconversions.reportCallback_();
+  return goog.html.SafeStyleSheet
+      .createSafeStyleSheetSecurityPrivateDoNotAccessOrElse(styleSheet);
+};
+
+
+/**
+ * Performs an "unchecked conversion" from string to SafeUrl for legacy API
+ * purposes.
+ *
+ * Please read fileoverview documentation before using.
+ *
+ * @param {string} url A string to be converted to SafeUrl.
+ * @return {!goog.html.SafeUrl} The value of url, wrapped in a SafeUrl
+ *     object.
+ */
+goog.html.legacyconversions.safeUrlFromString = function(url) {
+  goog.html.legacyconversions.reportCallback_();
+  return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(url);
+};
+
+
+/**
+ * Performs an "unchecked conversion" from string to TrustedResourceUrl for
+ * legacy API purposes.
+ *
+ * Please read fileoverview documentation before using.
+ *
+ * @param {string} url A string to be converted to TrustedResourceUrl.
+ * @return {!goog.html.TrustedResourceUrl} The value of url, wrapped in a
+ *     TrustedResourceUrl object.
+ */
+goog.html.legacyconversions.trustedResourceUrlFromString = function(url) {
+  goog.html.legacyconversions.reportCallback_();
+  return goog.html.TrustedResourceUrl
+      .createTrustedResourceUrlSecurityPrivateDoNotAccessOrElse(url);
+};
+
+/**
+ * @private {function(): undefined}
+ */
+goog.html.legacyconversions.reportCallback_ = goog.nullFunction;
+
+
+/**
+ * Sets a function that will be called every time a legacy conversion is
+ * performed. The function is called with no parameters but it can use
+ * goog.debug.getStacktrace to get a stacktrace.
+ *
+ * @param {function(): undefined} callback Error callback as defined above.
+ */
+goog.html.legacyconversions.setReportCallback = function(callback) {
+  goog.html.legacyconversions.reportCallback_ = callback;
+};
diff --git a/third_party/ink/closure/html/safehtml.js b/third_party/ink/closure/html/safehtml.js
new file mode 100644
index 0000000..088e014
--- /dev/null
+++ b/third_party/ink/closure/html/safehtml.js
@@ -0,0 +1,994 @@
+// Copyright 2013 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 The SafeHtml type and its builders.
+ *
+ * TODO(xtof): Link to document stating type contract.
+ */
+
+goog.provide('goog.html.SafeHtml');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.dom.TagName');
+goog.require('goog.dom.tags');
+goog.require('goog.html.SafeScript');
+goog.require('goog.html.SafeStyle');
+goog.require('goog.html.SafeStyleSheet');
+goog.require('goog.html.SafeUrl');
+goog.require('goog.html.TrustedResourceUrl');
+goog.require('goog.i18n.bidi.Dir');
+goog.require('goog.i18n.bidi.DirectionalString');
+goog.require('goog.labs.userAgent.browser');
+goog.require('goog.object');
+goog.require('goog.string');
+goog.require('goog.string.Const');
+goog.require('goog.string.TypedString');
+
+
+
+/**
+ * A string that is safe to use in HTML context in DOM APIs and HTML documents.
+ *
+ * A SafeHtml is a string-like object that carries the security type contract
+ * that its value as a string will not cause untrusted script execution when
+ * evaluated as HTML in a browser.
+ *
+ * Values of this type are guaranteed to be safe to use in HTML contexts,
+ * such as, assignment to the innerHTML DOM property, or interpolation into
+ * a HTML template in HTML PC_DATA context, in the sense that the use will not
+ * result in a Cross-Site-Scripting vulnerability.
+ *
+ * Instances of this type must be created via the factory methods
+ * ({@code goog.html.SafeHtml.create}, {@code goog.html.SafeHtml.htmlEscape}),
+ * etc and not by invoking its constructor.  The constructor intentionally
+ * takes no parameters and the type is immutable; hence only a default instance
+ * corresponding to the empty string can be obtained via constructor invocation.
+ *
+ * @see goog.html.SafeHtml#create
+ * @see goog.html.SafeHtml#htmlEscape
+ * @constructor
+ * @final
+ * @struct
+ * @implements {goog.i18n.bidi.DirectionalString}
+ * @implements {goog.string.TypedString}
+ */
+goog.html.SafeHtml = function() {
+  /**
+   * The contained value of this SafeHtml.  The field has a purposely ugly
+   * name to make (non-compiled) code that attempts to directly access this
+   * field stand out.
+   * @private {string}
+   */
+  this.privateDoNotAccessOrElseSafeHtmlWrappedValue_ = '';
+
+  /**
+   * A type marker used to implement additional run-time type checking.
+   * @see goog.html.SafeHtml#unwrap
+   * @const {!Object}
+   * @private
+   */
+  this.SAFE_HTML_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ =
+      goog.html.SafeHtml.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_;
+
+  /**
+   * This SafeHtml's directionality, or null if unknown.
+   * @private {?goog.i18n.bidi.Dir}
+   */
+  this.dir_ = null;
+};
+
+
+/**
+ * @override
+ * @const
+ */
+goog.html.SafeHtml.prototype.implementsGoogI18nBidiDirectionalString = true;
+
+
+/** @override */
+goog.html.SafeHtml.prototype.getDirection = function() {
+  return this.dir_;
+};
+
+
+/**
+ * @override
+ * @const
+ */
+goog.html.SafeHtml.prototype.implementsGoogStringTypedString = true;
+
+
+/**
+ * Returns this SafeHtml's value as string.
+ *
+ * IMPORTANT: In code where it is security relevant that an object's type is
+ * indeed {@code SafeHtml}, use {@code goog.html.SafeHtml.unwrap} instead of
+ * this method. If in doubt, assume that it's security relevant. In particular,
+ * note that goog.html functions which return a goog.html type do not guarantee
+ * that the returned instance is of the right type. For example:
+ *
+ * <pre>
+ * var fakeSafeHtml = new String('fake');
+ * fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;
+ * var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);
+ * // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by
+ * // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml
+ * // instanceof goog.html.SafeHtml.
+ * </pre>
+ *
+ * @see goog.html.SafeHtml#unwrap
+ * @override
+ */
+goog.html.SafeHtml.prototype.getTypedStringValue = function() {
+  return this.privateDoNotAccessOrElseSafeHtmlWrappedValue_;
+};
+
+
+if (goog.DEBUG) {
+  /**
+   * Returns a debug string-representation of this value.
+   *
+   * To obtain the actual string value wrapped in a SafeHtml, use
+   * {@code goog.html.SafeHtml.unwrap}.
+   *
+   * @see goog.html.SafeHtml#unwrap
+   * @override
+   */
+  goog.html.SafeHtml.prototype.toString = function() {
+    return 'SafeHtml{' + this.privateDoNotAccessOrElseSafeHtmlWrappedValue_ +
+        '}';
+  };
+}
+
+
+/**
+ * Performs a runtime check that the provided object is indeed a SafeHtml
+ * object, and returns its value.
+ * @param {!goog.html.SafeHtml} safeHtml The object to extract from.
+ * @return {string} The SafeHtml object's contained string, unless the run-time
+ *     type check fails. In that case, {@code unwrap} returns an innocuous
+ *     string, or, if assertions are enabled, throws
+ *     {@code goog.asserts.AssertionError}.
+ */
+goog.html.SafeHtml.unwrap = function(safeHtml) {
+  // Perform additional run-time type-checking to ensure that safeHtml is indeed
+  // an instance of the expected type.  This provides some additional protection
+  // against security bugs due to application code that disables type checks.
+  // Specifically, the following checks are performed:
+  // 1. The object is an instance of the expected type.
+  // 2. The object is not an instance of a subclass.
+  // 3. The object carries a type marker for the expected type. "Faking" an
+  // object requires a reference to the type marker, which has names intended
+  // to stand out in code reviews.
+  if (safeHtml instanceof goog.html.SafeHtml &&
+      safeHtml.constructor === goog.html.SafeHtml &&
+      safeHtml.SAFE_HTML_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ ===
+          goog.html.SafeHtml.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_) {
+    return safeHtml.privateDoNotAccessOrElseSafeHtmlWrappedValue_;
+  } else {
+    goog.asserts.fail('expected object of type SafeHtml, got \'' +
+        safeHtml + '\' of type ' + goog.typeOf(safeHtml));
+    return 'type_error:SafeHtml';
+  }
+};
+
+
+/**
+ * Shorthand for union of types that can sensibly be converted to strings
+ * or might already be SafeHtml (as SafeHtml is a goog.string.TypedString).
+ * @private
+ * @typedef {string|number|boolean|!goog.string.TypedString|
+ *           !goog.i18n.bidi.DirectionalString}
+ */
+goog.html.SafeHtml.TextOrHtml_;
+
+
+/**
+ * Returns HTML-escaped text as a SafeHtml object.
+ *
+ * If text is of a type that implements
+ * {@code goog.i18n.bidi.DirectionalString}, the directionality of the new
+ * {@code SafeHtml} object is set to {@code text}'s directionality, if known.
+ * Otherwise, the directionality of the resulting SafeHtml is unknown (i.e.,
+ * {@code null}).
+ *
+ * @param {!goog.html.SafeHtml.TextOrHtml_} textOrHtml The text to escape. If
+ *     the parameter is of type SafeHtml it is returned directly (no escaping
+ *     is done).
+ * @return {!goog.html.SafeHtml} The escaped text, wrapped as a SafeHtml.
+ */
+goog.html.SafeHtml.htmlEscape = function(textOrHtml) {
+  if (textOrHtml instanceof goog.html.SafeHtml) {
+    return textOrHtml;
+  }
+  var dir = null;
+  if (textOrHtml.implementsGoogI18nBidiDirectionalString) {
+    dir = textOrHtml.getDirection();
+  }
+  var textAsString;
+  if (textOrHtml.implementsGoogStringTypedString) {
+    textAsString = textOrHtml.getTypedStringValue();
+  } else {
+    textAsString = String(textOrHtml);
+  }
+  return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
+      goog.string.htmlEscape(textAsString), dir);
+};
+
+
+/**
+ * Returns HTML-escaped text as a SafeHtml object, with newlines changed to
+ * &lt;br&gt;.
+ * @param {!goog.html.SafeHtml.TextOrHtml_} textOrHtml The text to escape. If
+ *     the parameter is of type SafeHtml it is returned directly (no escaping
+ *     is done).
+ * @return {!goog.html.SafeHtml} The escaped text, wrapped as a SafeHtml.
+ */
+goog.html.SafeHtml.htmlEscapePreservingNewlines = function(textOrHtml) {
+  if (textOrHtml instanceof goog.html.SafeHtml) {
+    return textOrHtml;
+  }
+  var html = goog.html.SafeHtml.htmlEscape(textOrHtml);
+  return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
+      goog.string.newLineToBr(goog.html.SafeHtml.unwrap(html)),
+      html.getDirection());
+};
+
+
+/**
+ * Returns HTML-escaped text as a SafeHtml object, with newlines changed to
+ * &lt;br&gt; and escaping whitespace to preserve spatial formatting. Character
+ * entity #160 is used to make it safer for XML.
+ * @param {!goog.html.SafeHtml.TextOrHtml_} textOrHtml The text to escape. If
+ *     the parameter is of type SafeHtml it is returned directly (no escaping
+ *     is done).
+ * @return {!goog.html.SafeHtml} The escaped text, wrapped as a SafeHtml.
+ */
+goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces = function(
+    textOrHtml) {
+  if (textOrHtml instanceof goog.html.SafeHtml) {
+    return textOrHtml;
+  }
+  var html = goog.html.SafeHtml.htmlEscape(textOrHtml);
+  return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
+      goog.string.whitespaceEscape(goog.html.SafeHtml.unwrap(html)),
+      html.getDirection());
+};
+
+
+/**
+ * Coerces an arbitrary object into a SafeHtml object.
+ *
+ * If {@code textOrHtml} is already of type {@code goog.html.SafeHtml}, the same
+ * object is returned. Otherwise, {@code textOrHtml} is coerced to string, and
+ * HTML-escaped. If {@code textOrHtml} is of a type that implements
+ * {@code goog.i18n.bidi.DirectionalString}, its directionality, if known, is
+ * preserved.
+ *
+ * @param {!goog.html.SafeHtml.TextOrHtml_} textOrHtml The text or SafeHtml to
+ *     coerce.
+ * @return {!goog.html.SafeHtml} The resulting SafeHtml object.
+ * @deprecated Use goog.html.SafeHtml.htmlEscape.
+ */
+goog.html.SafeHtml.from = goog.html.SafeHtml.htmlEscape;
+
+
+/**
+ * @const
+ * @private
+ */
+goog.html.SafeHtml.VALID_NAMES_IN_TAG_ = /^[a-zA-Z0-9-]+$/;
+
+
+/**
+ * Set of attributes containing URL as defined at
+ * http://www.w3.org/TR/html5/index.html#attributes-1.
+ * @private @const {!Object<string,boolean>}
+ */
+goog.html.SafeHtml.URL_ATTRIBUTES_ = goog.object.createSet(
+    'action', 'cite', 'data', 'formaction', 'href', 'manifest', 'poster',
+    'src');
+
+
+/**
+ * Tags which are unsupported via create(). They might be supported via a
+ * tag-specific create method. These are tags which might require a
+ * TrustedResourceUrl in one of their attributes or a restricted type for
+ * their content.
+ * @private @const {!Object<string,boolean>}
+ */
+goog.html.SafeHtml.NOT_ALLOWED_TAG_NAMES_ = goog.object.createSet(
+    goog.dom.TagName.APPLET, goog.dom.TagName.BASE, goog.dom.TagName.EMBED,
+    goog.dom.TagName.IFRAME, goog.dom.TagName.LINK, goog.dom.TagName.MATH,
+    goog.dom.TagName.META, goog.dom.TagName.OBJECT, goog.dom.TagName.SCRIPT,
+    goog.dom.TagName.STYLE, goog.dom.TagName.SVG, goog.dom.TagName.TEMPLATE);
+
+
+/**
+ * @typedef {string|number|goog.string.TypedString|
+ *     goog.html.SafeStyle.PropertyMap|undefined}
+ */
+goog.html.SafeHtml.AttributeValue;
+
+
+/**
+ * Creates a SafeHtml content consisting of a tag with optional attributes and
+ * optional content.
+ *
+ * For convenience tag names and attribute names are accepted as regular
+ * strings, instead of goog.string.Const. Nevertheless, you should not pass
+ * user-controlled values to these parameters. Note that these parameters are
+ * syntactically validated at runtime, and invalid values will result in
+ * an exception.
+ *
+ * Example usage:
+ *
+ * goog.html.SafeHtml.create('br');
+ * goog.html.SafeHtml.create('div', {'class': 'a'});
+ * goog.html.SafeHtml.create('p', {}, 'a');
+ * goog.html.SafeHtml.create('p', {}, goog.html.SafeHtml.create('br'));
+ *
+ * goog.html.SafeHtml.create('span', {
+ *   'style': {'margin': '0'}
+ * });
+ *
+ * To guarantee SafeHtml's type contract is upheld there are restrictions on
+ * attribute values and tag names.
+ *
+ * - For attributes which contain script code (on*), a goog.string.Const is
+ *   required.
+ * - For attributes which contain style (style), a goog.html.SafeStyle or a
+ *   goog.html.SafeStyle.PropertyMap is required.
+ * - For attributes which are interpreted as URLs (e.g. src, href) a
+ *   goog.html.SafeUrl, goog.string.Const or string is required. If a string
+ *   is passed, it will be sanitized with SafeUrl.sanitize().
+ * - For tags which can load code or set security relevant page metadata,
+ *   more specific goog.html.SafeHtml.create*() functions must be used. Tags
+ *   which are not supported by this function are applet, base, embed, iframe,
+ *   link, math, object, script, style, svg, and template.
+ *
+ * @param {!goog.dom.TagName|string} tagName The name of the tag. Only tag names
+ *     consisting of [a-zA-Z0-9-] are allowed. Tag names documented above are
+ *     disallowed.
+ * @param {?Object<string, ?goog.html.SafeHtml.AttributeValue>=} opt_attributes
+ *     Mapping from attribute names to their values. Only attribute names
+ *     consisting of [a-zA-Z0-9-] are allowed. Value of null or undefined causes
+ *     the attribute to be omitted.
+ * @param {!goog.html.SafeHtml.TextOrHtml_|
+ *     !Array<!goog.html.SafeHtml.TextOrHtml_>=} opt_content Content to
+ *     HTML-escape and put inside the tag. This must be empty for void tags
+ *     like <br>. Array elements are concatenated.
+ * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
+ * @throws {Error} If invalid tag name, attribute name, or attribute value is
+ *     provided.
+ * @throws {goog.asserts.AssertionError} If content for void tag is provided.
+ */
+goog.html.SafeHtml.create = function(tagName, opt_attributes, opt_content) {
+  goog.html.SafeHtml.verifyTagName(String(tagName));
+  return goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse(
+      String(tagName), opt_attributes, opt_content);
+};
+
+
+/**
+ * Verifies if the tag name is valid and if it doesn't change the context.
+ * E.g. STRONG is fine but SCRIPT throws because it changes context. See
+ * goog.html.SafeHtml.create for an explanation of allowed tags.
+ * @param {string} tagName
+ * @throws {Error} If invalid tag name is provided.
+ * @package
+ */
+goog.html.SafeHtml.verifyTagName = function(tagName) {
+  if (!goog.html.SafeHtml.VALID_NAMES_IN_TAG_.test(tagName)) {
+    throw new Error('Invalid tag name <' + tagName + '>.');
+  }
+  if (tagName.toUpperCase() in goog.html.SafeHtml.NOT_ALLOWED_TAG_NAMES_) {
+    throw new Error('Tag name <' + tagName + '> is not allowed for SafeHtml.');
+  }
+};
+
+
+/**
+ * Creates a SafeHtml representing an iframe tag.
+ *
+ * This by default restricts the iframe as much as possible by setting the
+ * sandbox attribute to the empty string. If the iframe requires less
+ * restrictions, set the sandbox attribute as tight as possible, but do not rely
+ * on the sandbox as a security feature because it is not supported by older
+ * browsers. If a sandbox is essential to security (e.g. for third-party
+ * frames), use createSandboxIframe which checks for browser support.
+ *
+ * @see https://developer.mozilla.org/en/docs/Web/HTML/Element/iframe#attr-sandbox
+ *
+ * @param {?goog.html.TrustedResourceUrl=} opt_src The value of the src
+ *     attribute. If null or undefined src will not be set.
+ * @param {?goog.html.SafeHtml=} opt_srcdoc The value of the srcdoc attribute.
+ *     If null or undefined srcdoc will not be set.
+ * @param {?Object<string, ?goog.html.SafeHtml.AttributeValue>=} opt_attributes
+ *     Mapping from attribute names to their values. Only attribute names
+ *     consisting of [a-zA-Z0-9-] are allowed. Value of null or undefined causes
+ *     the attribute to be omitted.
+ * @param {!goog.html.SafeHtml.TextOrHtml_|
+ *     !Array<!goog.html.SafeHtml.TextOrHtml_>=} opt_content Content to
+ *     HTML-escape and put inside the tag. Array elements are concatenated.
+ * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
+ * @throws {Error} If invalid tag name, attribute name, or attribute value is
+ *     provided. If opt_attributes contains the src or srcdoc attributes.
+ */
+goog.html.SafeHtml.createIframe = function(
+    opt_src, opt_srcdoc, opt_attributes, opt_content) {
+  if (opt_src) {
+    // Check whether this is really TrustedResourceUrl.
+    goog.html.TrustedResourceUrl.unwrap(opt_src);
+  }
+
+  var fixedAttributes = {};
+  fixedAttributes['src'] = opt_src || null;
+  fixedAttributes['srcdoc'] =
+      opt_srcdoc && goog.html.SafeHtml.unwrap(opt_srcdoc);
+  var defaultAttributes = {'sandbox': ''};
+  var attributes = goog.html.SafeHtml.combineAttributes(
+      fixedAttributes, defaultAttributes, opt_attributes);
+  return goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse(
+      'iframe', attributes, opt_content);
+};
+
+
+/**
+ * Creates a SafeHtml representing a sandboxed iframe tag.
+ *
+ * The sandbox attribute is enforced in its most restrictive mode, an empty
+ * string. Consequently, the security requirements for the src and srcdoc
+ * attributes are relaxed compared to SafeHtml.createIframe. This function
+ * will throw on browsers that do not support the sandbox attribute, as
+ * determined by SafeHtml.canUseSandboxIframe.
+ *
+ * The SafeHtml returned by this function can trigger downloads with no
+ * user interaction on Chrome (though only a few, further attempts are blocked).
+ * Firefox and IE will block all downloads from the sandbox.
+ *
+ * @see https://developer.mozilla.org/en/docs/Web/HTML/Element/iframe#attr-sandbox
+ * @see https://lists.w3.org/Archives/Public/public-whatwg-archive/2013Feb/0112.html
+ *
+ * @param {string|!goog.html.SafeUrl=} opt_src The value of the src
+ *     attribute. If null or undefined src will not be set.
+ * @param {string=} opt_srcdoc The value of the srcdoc attribute.
+ *     If null or undefined srcdoc will not be set. Will not be sanitized.
+ * @param {!Object<string, ?goog.html.SafeHtml.AttributeValue>=} opt_attributes
+ *     Mapping from attribute names to their values. Only attribute names
+ *     consisting of [a-zA-Z0-9-] are allowed. Value of null or undefined causes
+ *     the attribute to be omitted.
+ * @param {!goog.html.SafeHtml.TextOrHtml_|
+ *     !Array<!goog.html.SafeHtml.TextOrHtml_>=} opt_content Content to
+ *     HTML-escape and put inside the tag. Array elements are concatenated.
+ * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
+ * @throws {Error} If invalid tag name, attribute name, or attribute value is
+ *     provided. If opt_attributes contains the src, srcdoc or sandbox
+ *     attributes. If browser does not support the sandbox attribute on iframe.
+ */
+goog.html.SafeHtml.createSandboxIframe = function(
+    opt_src, opt_srcdoc, opt_attributes, opt_content) {
+  if (!goog.html.SafeHtml.canUseSandboxIframe()) {
+    throw new Error('The browser does not support sandboxed iframes.');
+  }
+
+  var fixedAttributes = {};
+  if (opt_src) {
+    // Note that sanitize is a no-op on SafeUrl.
+    fixedAttributes['src'] =
+        goog.html.SafeUrl.unwrap(goog.html.SafeUrl.sanitize(opt_src));
+  } else {
+    fixedAttributes['src'] = null;
+  }
+  fixedAttributes['srcdoc'] = opt_srcdoc || null;
+  fixedAttributes['sandbox'] = '';
+  var attributes =
+      goog.html.SafeHtml.combineAttributes(fixedAttributes, {}, opt_attributes);
+  return goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse(
+      'iframe', attributes, opt_content);
+};
+
+
+/**
+ * Checks if the user agent supports sandboxed iframes.
+ * @return {boolean}
+ */
+goog.html.SafeHtml.canUseSandboxIframe = function() {
+  return goog.global['HTMLIFrameElement'] &&
+      ('sandbox' in goog.global['HTMLIFrameElement'].prototype);
+};
+
+
+/**
+ * Creates a SafeHtml representing a script tag with the src attribute.
+ * @param {!goog.html.TrustedResourceUrl} src The value of the src
+ * attribute.
+ * @param {?Object<string, ?goog.html.SafeHtml.AttributeValue>=}
+ * opt_attributes
+ *     Mapping from attribute names to their values. Only attribute names
+ *     consisting of [a-zA-Z0-9-] are allowed. Value of null or undefined
+ *     causes the attribute to be omitted.
+ * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
+ * @throws {Error} If invalid attribute name or value is provided. If
+ *     opt_attributes contains the src attribute.
+ */
+goog.html.SafeHtml.createScriptSrc = function(src, opt_attributes) {
+  // TODO(mlourenco): The charset attribute should probably be blocked. If
+  // its value is attacker controlled, the script contains attacker controlled
+  // sub-strings (even if properly escaped) and the server does not set charset
+  // then XSS is likely possible.
+  // https://html.spec.whatwg.org/multipage/scripting.html#dom-script-charset
+
+  // Check whether this is really TrustedResourceUrl.
+  goog.html.TrustedResourceUrl.unwrap(src);
+
+  var fixedAttributes = {'src': src};
+  var defaultAttributes = {};
+  var attributes = goog.html.SafeHtml.combineAttributes(
+      fixedAttributes, defaultAttributes, opt_attributes);
+  return goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse(
+      'script', attributes);
+};
+
+
+/**
+ * Creates a SafeHtml representing a script tag. Does not allow the language,
+ * src, text or type attributes to be set.
+ * @param {!goog.html.SafeScript|!Array<!goog.html.SafeScript>}
+ *     script Content to put inside the tag. Array elements are
+ *     concatenated.
+ * @param {?Object<string, ?goog.html.SafeHtml.AttributeValue>=} opt_attributes
+ *     Mapping from attribute names to their values. Only attribute names
+ *     consisting of [a-zA-Z0-9-] are allowed. Value of null or undefined causes
+ *     the attribute to be omitted.
+ * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
+ * @throws {Error} If invalid attribute name or attribute value is provided. If
+ *     opt_attributes contains the language, src, text or type attribute.
+ */
+goog.html.SafeHtml.createScript = function(script, opt_attributes) {
+  for (var attr in opt_attributes) {
+    var attrLower = attr.toLowerCase();
+    if (attrLower == 'language' || attrLower == 'src' || attrLower == 'text' ||
+        attrLower == 'type') {
+      throw new Error('Cannot set "' + attrLower + '" attribute');
+    }
+  }
+
+  var content = '';
+  script = goog.array.concat(script);
+  for (var i = 0; i < script.length; i++) {
+    content += goog.html.SafeScript.unwrap(script[i]);
+  }
+  // Convert to SafeHtml so that it's not HTML-escaped. This is safe because
+  // as part of its contract, SafeScript should have no dangerous '<'.
+  var htmlContent =
+      goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
+          content, goog.i18n.bidi.Dir.NEUTRAL);
+  return goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse(
+      'script', opt_attributes, htmlContent);
+};
+
+
+/**
+ * Creates a SafeHtml representing a style tag. The type attribute is set
+ * to "text/css".
+ * @param {!goog.html.SafeStyleSheet|!Array<!goog.html.SafeStyleSheet>}
+ *     styleSheet Content to put inside the tag. Array elements are
+ *     concatenated.
+ * @param {?Object<string, ?goog.html.SafeHtml.AttributeValue>=} opt_attributes
+ *     Mapping from attribute names to their values. Only attribute names
+ *     consisting of [a-zA-Z0-9-] are allowed. Value of null or undefined causes
+ *     the attribute to be omitted.
+ * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
+ * @throws {Error} If invalid attribute name or attribute value is provided. If
+ *     opt_attributes contains the type attribute.
+ */
+goog.html.SafeHtml.createStyle = function(styleSheet, opt_attributes) {
+  var fixedAttributes = {'type': 'text/css'};
+  var defaultAttributes = {};
+  var attributes = goog.html.SafeHtml.combineAttributes(
+      fixedAttributes, defaultAttributes, opt_attributes);
+
+  var content = '';
+  styleSheet = goog.array.concat(styleSheet);
+  for (var i = 0; i < styleSheet.length; i++) {
+    content += goog.html.SafeStyleSheet.unwrap(styleSheet[i]);
+  }
+  // Convert to SafeHtml so that it's not HTML-escaped. This is safe because
+  // as part of its contract, SafeStyleSheet should have no dangerous '<'.
+  var htmlContent =
+      goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
+          content, goog.i18n.bidi.Dir.NEUTRAL);
+  return goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse(
+      'style', attributes, htmlContent);
+};
+
+
+/**
+ * Creates a SafeHtml representing a meta refresh tag.
+ * @param {!goog.html.SafeUrl|string} url Where to redirect. If a string is
+ *     passed, it will be sanitized with SafeUrl.sanitize().
+ * @param {number=} opt_secs Number of seconds until the page should be
+ *     reloaded. Will be set to 0 if unspecified.
+ * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
+ */
+goog.html.SafeHtml.createMetaRefresh = function(url, opt_secs) {
+
+  // Note that sanitize is a no-op on SafeUrl.
+  var unwrappedUrl = goog.html.SafeUrl.unwrap(goog.html.SafeUrl.sanitize(url));
+
+  if (goog.labs.userAgent.browser.isIE() ||
+      goog.labs.userAgent.browser.isEdge()) {
+    // IE/EDGE can't parse the content attribute if the url contains a
+    // semicolon. We can fix this by adding quotes around the url, but then we
+    // can't parse quotes in the URL correctly. Also, it seems that IE/EDGE
+    // did not unescape semicolons in these URLs at some point in the past. We
+    // take a best-effort approach.
+    //
+    // If the URL has semicolons (which may happen in some cases, see
+    // http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.2
+    // for instance), wrap it in single quotes to protect the semicolons.
+    // If the URL has semicolons and single quotes, url-encode the single quotes
+    // as well.
+    //
+    // This is imperfect. Notice that both ' and ; are reserved characters in
+    // URIs, so this could do the wrong thing, but at least it will do the wrong
+    // thing in only rare cases.
+    if (goog.string.contains(unwrappedUrl, ';')) {
+      unwrappedUrl = "'" + unwrappedUrl.replace(/'/g, '%27') + "'";
+    }
+  }
+  var attributes = {
+    'http-equiv': 'refresh',
+    'content': (opt_secs || 0) + '; url=' + unwrappedUrl
+  };
+
+  // This function will handle the HTML escaping for attributes.
+  return goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse(
+      'meta', attributes);
+};
+
+
+/**
+ * @param {string} tagName The tag name.
+ * @param {string} name The attribute name.
+ * @param {!goog.html.SafeHtml.AttributeValue} value The attribute value.
+ * @return {string} A "name=value" string.
+ * @throws {Error} If attribute value is unsafe for the given tag and attribute.
+ * @private
+ */
+goog.html.SafeHtml.getAttrNameAndValue_ = function(tagName, name, value) {
+  // If it's goog.string.Const, allow any valid attribute name.
+  if (value instanceof goog.string.Const) {
+    value = goog.string.Const.unwrap(value);
+  } else if (name.toLowerCase() == 'style') {
+    value = goog.html.SafeHtml.getStyleValue_(value);
+  } else if (/^on/i.test(name)) {
+    // TODO(jakubvrana): Disallow more attributes with a special meaning.
+    throw new Error(
+        'Attribute "' + name + '" requires goog.string.Const value, "' + value +
+        '" given.');
+    // URL attributes handled differently according to tag.
+  } else if (name.toLowerCase() in goog.html.SafeHtml.URL_ATTRIBUTES_) {
+    if (value instanceof goog.html.TrustedResourceUrl) {
+      value = goog.html.TrustedResourceUrl.unwrap(value);
+    } else if (value instanceof goog.html.SafeUrl) {
+      value = goog.html.SafeUrl.unwrap(value);
+    } else if (goog.isString(value)) {
+      value = goog.html.SafeUrl.sanitize(value).getTypedStringValue();
+    } else {
+      throw new Error(
+          'Attribute "' + name + '" on tag "' + tagName +
+          '" requires goog.html.SafeUrl, goog.string.Const, or string,' +
+          ' value "' + value + '" given.');
+    }
+  }
+
+  // Accept SafeUrl, TrustedResourceUrl, etc. for attributes which only require
+  // HTML-escaping.
+  if (value.implementsGoogStringTypedString) {
+    // Ok to call getTypedStringValue() since there's no reliance on the type
+    // contract for security here.
+    value = value.getTypedStringValue();
+  }
+
+  goog.asserts.assert(
+      goog.isString(value) || goog.isNumber(value),
+      'String or number value expected, got ' + (typeof value) +
+          ' with value: ' + value);
+  return name + '="' + goog.string.htmlEscape(String(value)) + '"';
+};
+
+
+/**
+ * Gets value allowed in "style" attribute.
+ * @param {!goog.html.SafeHtml.AttributeValue} value It could be SafeStyle or a
+ *     map which will be passed to goog.html.SafeStyle.create.
+ * @return {string} Unwrapped value.
+ * @throws {Error} If string value is given.
+ * @private
+ */
+goog.html.SafeHtml.getStyleValue_ = function(value) {
+  if (!goog.isObject(value)) {
+    throw new Error(
+        'The "style" attribute requires goog.html.SafeStyle or map ' +
+        'of style properties, ' + (typeof value) + ' given: ' + value);
+  }
+  if (!(value instanceof goog.html.SafeStyle)) {
+    // Process the property bag into a style object.
+    value = goog.html.SafeStyle.create(value);
+  }
+  return goog.html.SafeStyle.unwrap(value);
+};
+
+
+/**
+ * Creates a SafeHtml content with known directionality consisting of a tag with
+ * optional attributes and optional content.
+ * @param {!goog.i18n.bidi.Dir} dir Directionality.
+ * @param {string} tagName
+ * @param {?Object<string, ?goog.html.SafeHtml.AttributeValue>=} opt_attributes
+ * @param {!goog.html.SafeHtml.TextOrHtml_|
+ *     !Array<!goog.html.SafeHtml.TextOrHtml_>=} opt_content
+ * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
+ */
+goog.html.SafeHtml.createWithDir = function(
+    dir, tagName, opt_attributes, opt_content) {
+  var html = goog.html.SafeHtml.create(tagName, opt_attributes, opt_content);
+  html.dir_ = dir;
+  return html;
+};
+
+
+/**
+ * Creates a new SafeHtml object by concatenating values.
+ * @param {...(!goog.html.SafeHtml.TextOrHtml_|
+ *     !Array<!goog.html.SafeHtml.TextOrHtml_>)} var_args Values to concatenate.
+ * @return {!goog.html.SafeHtml}
+ */
+goog.html.SafeHtml.concat = function(var_args) {
+  var dir = goog.i18n.bidi.Dir.NEUTRAL;
+  var content = '';
+
+  /**
+   * @param {!goog.html.SafeHtml.TextOrHtml_|
+   *     !Array<!goog.html.SafeHtml.TextOrHtml_>} argument
+   */
+  var addArgument = function(argument) {
+    if (goog.isArray(argument)) {
+      goog.array.forEach(argument, addArgument);
+    } else {
+      var html = goog.html.SafeHtml.htmlEscape(argument);
+      content += goog.html.SafeHtml.unwrap(html);
+      var htmlDir = html.getDirection();
+      if (dir == goog.i18n.bidi.Dir.NEUTRAL) {
+        dir = htmlDir;
+      } else if (htmlDir != goog.i18n.bidi.Dir.NEUTRAL && dir != htmlDir) {
+        dir = null;
+      }
+    }
+  };
+
+  goog.array.forEach(arguments, addArgument);
+  return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
+      content, dir);
+};
+
+
+/**
+ * Creates a new SafeHtml object with known directionality by concatenating the
+ * values.
+ * @param {!goog.i18n.bidi.Dir} dir Directionality.
+ * @param {...(!goog.html.SafeHtml.TextOrHtml_|
+ *     !Array<!goog.html.SafeHtml.TextOrHtml_>)} var_args Elements of array
+ *     arguments would be processed recursively.
+ * @return {!goog.html.SafeHtml}
+ */
+goog.html.SafeHtml.concatWithDir = function(dir, var_args) {
+  var html = goog.html.SafeHtml.concat(goog.array.slice(arguments, 1));
+  html.dir_ = dir;
+  return html;
+};
+
+
+/**
+ * Type marker for the SafeHtml type, used to implement additional run-time
+ * type checking.
+ * @const {!Object}
+ * @private
+ */
+goog.html.SafeHtml.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {};
+
+
+/**
+ * Package-internal utility method to create SafeHtml instances.
+ *
+ * @param {string} html The string to initialize the SafeHtml object with.
+ * @param {?goog.i18n.bidi.Dir} dir The directionality of the SafeHtml to be
+ *     constructed, or null if unknown.
+ * @return {!goog.html.SafeHtml} The initialized SafeHtml object.
+ * @package
+ */
+goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse = function(
+    html, dir) {
+  return new goog.html.SafeHtml().initSecurityPrivateDoNotAccessOrElse_(
+      html, dir);
+};
+
+
+/**
+ * Called from createSafeHtmlSecurityPrivateDoNotAccessOrElse(). This
+ * method exists only so that the compiler can dead code eliminate static
+ * fields (like EMPTY) when they're not accessed.
+ * @param {string} html
+ * @param {?goog.i18n.bidi.Dir} dir
+ * @return {!goog.html.SafeHtml}
+ * @private
+ */
+goog.html.SafeHtml.prototype.initSecurityPrivateDoNotAccessOrElse_ = function(
+    html, dir) {
+  this.privateDoNotAccessOrElseSafeHtmlWrappedValue_ = html;
+  this.dir_ = dir;
+  return this;
+};
+
+
+/**
+ * Like create() but does not restrict which tags can be constructed.
+ *
+ * @param {string} tagName Tag name. Set or validated by caller.
+ * @param {?Object<string, ?goog.html.SafeHtml.AttributeValue>=} opt_attributes
+ * @param {(!goog.html.SafeHtml.TextOrHtml_|
+ *     !Array<!goog.html.SafeHtml.TextOrHtml_>)=} opt_content
+ * @return {!goog.html.SafeHtml}
+ * @throws {Error} If invalid or unsafe attribute name or value is provided.
+ * @throws {goog.asserts.AssertionError} If content for void tag is provided.
+ * @package
+ */
+goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse = function(
+    tagName, opt_attributes, opt_content) {
+  var dir = null;
+  var result = '<' + tagName;
+  result += goog.html.SafeHtml.stringifyAttributes(tagName, opt_attributes);
+
+  var content = opt_content;
+  if (!goog.isDefAndNotNull(content)) {
+    content = [];
+  } else if (!goog.isArray(content)) {
+    content = [content];
+  }
+
+  if (goog.dom.tags.isVoidTag(tagName.toLowerCase())) {
+    goog.asserts.assert(
+        !content.length, 'Void tag <' + tagName + '> does not allow content.');
+    result += '>';
+  } else {
+    var html = goog.html.SafeHtml.concat(content);
+    result += '>' + goog.html.SafeHtml.unwrap(html) + '</' + tagName + '>';
+    dir = html.getDirection();
+  }
+
+  var dirAttribute = opt_attributes && opt_attributes['dir'];
+  if (dirAttribute) {
+    if (/^(ltr|rtl|auto)$/i.test(dirAttribute)) {
+      // If the tag has the "dir" attribute specified then its direction is
+      // neutral because it can be safely used in any context.
+      dir = goog.i18n.bidi.Dir.NEUTRAL;
+    } else {
+      dir = null;
+    }
+  }
+
+  return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
+      result, dir);
+};
+
+
+/**
+ * Creates a string with attributes to insert after tagName.
+ * @param {string} tagName
+ * @param {?Object<string, ?goog.html.SafeHtml.AttributeValue>=} opt_attributes
+ * @return {string} Returns an empty string if there are no attributes, returns
+ *     a string starting with a space otherwise.
+ * @throws {Error} If attribute value is unsafe for the given tag and attribute.
+ * @package
+ */
+goog.html.SafeHtml.stringifyAttributes = function(tagName, opt_attributes) {
+  var result = '';
+  if (opt_attributes) {
+    for (var name in opt_attributes) {
+      if (!goog.html.SafeHtml.VALID_NAMES_IN_TAG_.test(name)) {
+        throw new Error('Invalid attribute name "' + name + '".');
+      }
+      var value = opt_attributes[name];
+      if (!goog.isDefAndNotNull(value)) {
+        continue;
+      }
+      result +=
+          ' ' + goog.html.SafeHtml.getAttrNameAndValue_(tagName, name, value);
+    }
+  }
+  return result;
+};
+
+
+/**
+ * @param {!Object<string, ?goog.html.SafeHtml.AttributeValue>} fixedAttributes
+ * @param {!Object<string, string>} defaultAttributes
+ * @param {?Object<string, ?goog.html.SafeHtml.AttributeValue>=} opt_attributes
+ *     Optional attributes passed to create*().
+ * @return {!Object<string, ?goog.html.SafeHtml.AttributeValue>}
+ * @throws {Error} If opt_attributes contains an attribute with the same name
+ *     as an attribute in fixedAttributes.
+ * @package
+ */
+goog.html.SafeHtml.combineAttributes = function(
+    fixedAttributes, defaultAttributes, opt_attributes) {
+  var combinedAttributes = {};
+  var name;
+
+  for (name in fixedAttributes) {
+    goog.asserts.assert(name.toLowerCase() == name, 'Must be lower case');
+    combinedAttributes[name] = fixedAttributes[name];
+  }
+  for (name in defaultAttributes) {
+    goog.asserts.assert(name.toLowerCase() == name, 'Must be lower case');
+    combinedAttributes[name] = defaultAttributes[name];
+  }
+
+  for (name in opt_attributes) {
+    var nameLower = name.toLowerCase();
+    if (nameLower in fixedAttributes) {
+      throw new Error(
+          'Cannot override "' + nameLower + '" attribute, got "' + name +
+          '" with value "' + opt_attributes[name] + '"');
+    }
+    if (nameLower in defaultAttributes) {
+      delete combinedAttributes[nameLower];
+    }
+    combinedAttributes[name] = opt_attributes[name];
+  }
+
+  return combinedAttributes;
+};
+
+
+/**
+ * A SafeHtml instance corresponding to the HTML doctype: "<!DOCTYPE html>".
+ * @const {!goog.html.SafeHtml}
+ */
+goog.html.SafeHtml.DOCTYPE_HTML =
+    goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
+        '<!DOCTYPE html>', goog.i18n.bidi.Dir.NEUTRAL);
+
+
+/**
+ * A SafeHtml instance corresponding to the empty string.
+ * @const {!goog.html.SafeHtml}
+ */
+goog.html.SafeHtml.EMPTY =
+    goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
+        '', goog.i18n.bidi.Dir.NEUTRAL);
+
+
+/**
+ * A SafeHtml instance corresponding to the <br> tag.
+ * @const {!goog.html.SafeHtml}
+ */
+goog.html.SafeHtml.BR =
+    goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
+        '<br>', goog.i18n.bidi.Dir.NEUTRAL);
diff --git a/third_party/ink/closure/html/safescript.js b/third_party/ink/closure/html/safescript.js
new file mode 100644
index 0000000..7a945eb
--- /dev/null
+++ b/third_party/ink/closure/html/safescript.js
@@ -0,0 +1,234 @@
+// Copyright 2014 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 The SafeScript type and its builders.
+ *
+ * TODO(xtof): Link to document stating type contract.
+ */
+
+goog.provide('goog.html.SafeScript');
+
+goog.require('goog.asserts');
+goog.require('goog.string.Const');
+goog.require('goog.string.TypedString');
+
+
+
+/**
+ * A string-like object which represents JavaScript code and that carries the
+ * security type contract that its value, as a string, will not cause execution
+ * of unconstrained attacker controlled code (XSS) when evaluated as JavaScript
+ * in a browser.
+ *
+ * Instances of this type must be created via the factory method
+ * {@code goog.html.SafeScript.fromConstant} and not by invoking its
+ * constructor. The constructor intentionally takes no parameters and the type
+ * is immutable; hence only a default instance corresponding to the empty string
+ * can be obtained via constructor invocation.
+ *
+ * A SafeScript's string representation can safely be interpolated as the
+ * content of a script element within HTML. The SafeScript string should not be
+ * escaped before interpolation.
+ *
+ * Note that the SafeScript might contain text that is attacker-controlled but
+ * that text should have been interpolated with appropriate escaping,
+ * sanitization and/or validation into the right location in the script, such
+ * that it is highly constrained in its effect (for example, it had to match a
+ * set of whitelisted words).
+ *
+ * A SafeScript can be constructed via security-reviewed unchecked
+ * conversions. In this case producers of SafeScript must ensure themselves that
+ * the SafeScript does not contain unsafe script. Note in particular that
+ * {@code &lt;} is dangerous, even when inside JavaScript strings, and so should
+ * always be forbidden or JavaScript escaped in user controlled input. For
+ * example, if {@code &lt;/script&gt;&lt;script&gt;evil&lt;/script&gt;"} were
+ * interpolated inside a JavaScript string, it would break out of the context
+ * of the original script element and {@code evil} would execute. Also note
+ * that within an HTML script (raw text) element, HTML character references,
+ * such as "&lt;" are not allowed. See
+ * http://www.w3.org/TR/html5/scripting-1.html#restrictions-for-contents-of-script-elements.
+ *
+ * @see goog.html.SafeScript#fromConstant
+ * @constructor
+ * @final
+ * @struct
+ * @implements {goog.string.TypedString}
+ */
+goog.html.SafeScript = function() {
+  /**
+   * The contained value of this SafeScript.  The field has a purposely
+   * ugly name to make (non-compiled) code that attempts to directly access this
+   * field stand out.
+   * @private {string}
+   */
+  this.privateDoNotAccessOrElseSafeScriptWrappedValue_ = '';
+
+  /**
+   * A type marker used to implement additional run-time type checking.
+   * @see goog.html.SafeScript#unwrap
+   * @const {!Object}
+   * @private
+   */
+  this.SAFE_SCRIPT_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ =
+      goog.html.SafeScript.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_;
+};
+
+
+/**
+ * @override
+ * @const
+ */
+goog.html.SafeScript.prototype.implementsGoogStringTypedString = true;
+
+
+/**
+ * Type marker for the SafeScript type, used to implement additional
+ * run-time type checking.
+ * @const {!Object}
+ * @private
+ */
+goog.html.SafeScript.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {};
+
+
+/**
+ * Creates a SafeScript object from a compile-time constant string.
+ *
+ * @param {!goog.string.Const} script A compile-time-constant string from which
+ *     to create a SafeScript.
+ * @return {!goog.html.SafeScript} A SafeScript object initialized to
+ *     {@code script}.
+ */
+goog.html.SafeScript.fromConstant = function(script) {
+  var scriptString = goog.string.Const.unwrap(script);
+  if (scriptString.length === 0) {
+    return goog.html.SafeScript.EMPTY;
+  }
+  return goog.html.SafeScript.createSafeScriptSecurityPrivateDoNotAccessOrElse(
+      scriptString);
+};
+
+
+/**
+ * Returns this SafeScript's value as a string.
+ *
+ * IMPORTANT: In code where it is security relevant that an object's type is
+ * indeed {@code SafeScript}, use {@code goog.html.SafeScript.unwrap} instead of
+ * this method. If in doubt, assume that it's security relevant. In particular,
+ * note that goog.html functions which return a goog.html type do not guarantee
+ * the returned instance is of the right type. For example:
+ *
+ * <pre>
+ * var fakeSafeHtml = new String('fake');
+ * fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;
+ * var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);
+ * // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by
+ * // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml
+ * // instanceof goog.html.SafeHtml.
+ * </pre>
+ *
+ * @see goog.html.SafeScript#unwrap
+ * @override
+ */
+goog.html.SafeScript.prototype.getTypedStringValue = function() {
+  return this.privateDoNotAccessOrElseSafeScriptWrappedValue_;
+};
+
+
+if (goog.DEBUG) {
+  /**
+   * Returns a debug string-representation of this value.
+   *
+   * To obtain the actual string value wrapped in a SafeScript, use
+   * {@code goog.html.SafeScript.unwrap}.
+   *
+   * @see goog.html.SafeScript#unwrap
+   * @override
+   */
+  goog.html.SafeScript.prototype.toString = function() {
+    return 'SafeScript{' +
+        this.privateDoNotAccessOrElseSafeScriptWrappedValue_ + '}';
+  };
+}
+
+
+/**
+ * Performs a runtime check that the provided object is indeed a
+ * SafeScript object, and returns its value.
+ *
+ * @param {!goog.html.SafeScript} safeScript The object to extract from.
+ * @return {string} The safeScript object's contained string, unless
+ *     the run-time type check fails. In that case, {@code unwrap} returns an
+ *     innocuous string, or, if assertions are enabled, throws
+ *     {@code goog.asserts.AssertionError}.
+ */
+goog.html.SafeScript.unwrap = function(safeScript) {
+  // Perform additional Run-time type-checking to ensure that
+  // safeScript is indeed an instance of the expected type.  This
+  // provides some additional protection against security bugs due to
+  // application code that disables type checks.
+  // Specifically, the following checks are performed:
+  // 1. The object is an instance of the expected type.
+  // 2. The object is not an instance of a subclass.
+  // 3. The object carries a type marker for the expected type. "Faking" an
+  // object requires a reference to the type marker, which has names intended
+  // to stand out in code reviews.
+  if (safeScript instanceof goog.html.SafeScript &&
+      safeScript.constructor === goog.html.SafeScript &&
+      safeScript.SAFE_SCRIPT_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ ===
+          goog.html.SafeScript.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_) {
+    return safeScript.privateDoNotAccessOrElseSafeScriptWrappedValue_;
+  } else {
+    goog.asserts.fail('expected object of type SafeScript, got \'' +
+        safeScript + '\' of type ' + goog.typeOf(safeScript));
+    return 'type_error:SafeScript';
+  }
+};
+
+
+/**
+ * Package-internal utility method to create SafeScript instances.
+ *
+ * @param {string} script The string to initialize the SafeScript object with.
+ * @return {!goog.html.SafeScript} The initialized SafeScript object.
+ * @package
+ */
+goog.html.SafeScript.createSafeScriptSecurityPrivateDoNotAccessOrElse =
+    function(script) {
+  return new goog.html.SafeScript().initSecurityPrivateDoNotAccessOrElse_(
+      script);
+};
+
+
+/**
+ * Called from createSafeScriptSecurityPrivateDoNotAccessOrElse(). This
+ * method exists only so that the compiler can dead code eliminate static
+ * fields (like EMPTY) when they're not accessed.
+ * @param {string} script
+ * @return {!goog.html.SafeScript}
+ * @private
+ */
+goog.html.SafeScript.prototype.initSecurityPrivateDoNotAccessOrElse_ = function(
+    script) {
+  this.privateDoNotAccessOrElseSafeScriptWrappedValue_ = script;
+  return this;
+};
+
+
+/**
+ * A SafeScript instance corresponding to the empty string.
+ * @const {!goog.html.SafeScript}
+ */
+goog.html.SafeScript.EMPTY =
+    goog.html.SafeScript.createSafeScriptSecurityPrivateDoNotAccessOrElse('');
diff --git a/third_party/ink/closure/html/safestyle.js b/third_party/ink/closure/html/safestyle.js
new file mode 100644
index 0000000..95ff10c1
--- /dev/null
+++ b/third_party/ink/closure/html/safestyle.js
@@ -0,0 +1,560 @@
+// Copyright 2014 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 The SafeStyle type and its builders.
+ *
+ * TODO(xtof): Link to document stating type contract.
+ */
+
+goog.provide('goog.html.SafeStyle');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.html.SafeUrl');
+goog.require('goog.string');
+goog.require('goog.string.Const');
+goog.require('goog.string.TypedString');
+
+
+
+/**
+ * A string-like object which represents a sequence of CSS declarations
+ * ({@code propertyName1: propertyvalue1; propertyName2: propertyValue2; ...})
+ * and that carries the security type contract that its value, as a string,
+ * will not cause untrusted script execution (XSS) when evaluated as CSS in a
+ * browser.
+ *
+ * Instances of this type must be created via the factory methods
+ * ({@code goog.html.SafeStyle.create} or
+ * {@code goog.html.SafeStyle.fromConstant}) and not by invoking its
+ * constructor. The constructor intentionally takes no parameters and the type
+ * is immutable; hence only a default instance corresponding to the empty string
+ * can be obtained via constructor invocation.
+ *
+ * SafeStyle's string representation can safely be:
+ * <ul>
+ *   <li>Interpolated as the content of a *quoted* HTML style attribute.
+ *       However, the SafeStyle string *must be HTML-attribute-escaped* before
+ *       interpolation.
+ *   <li>Interpolated as the content of a {}-wrapped block within a stylesheet.
+ *       '<' characters in the SafeStyle string *must be CSS-escaped* before
+ *       interpolation. The SafeStyle string is also guaranteed not to be able
+ *       to introduce new properties or elide existing ones.
+ *   <li>Interpolated as the content of a {}-wrapped block within an HTML
+ *       <style> element. '<' characters in the SafeStyle string
+ *       *must be CSS-escaped* before interpolation.
+ *   <li>Assigned to the style property of a DOM node. The SafeStyle string
+ *       should not be escaped before being assigned to the property.
+ * </ul>
+ *
+ * A SafeStyle may never contain literal angle brackets. Otherwise, it could
+ * be unsafe to place a SafeStyle into a &lt;style&gt; tag (where it can't
+ * be HTML escaped). For example, if the SafeStyle containing
+ * "{@code font: 'foo &lt;style/&gt;&lt;script&gt;evil&lt;/script&gt;'}" were
+ * interpolated within a &lt;style&gt; tag, this would then break out of the
+ * style context into HTML.
+ *
+ * A SafeStyle may contain literal single or double quotes, and as such the
+ * entire style string must be escaped when used in a style attribute (if
+ * this were not the case, the string could contain a matching quote that
+ * would escape from the style attribute).
+ *
+ * Values of this type must be composable, i.e. for any two values
+ * {@code style1} and {@code style2} of this type,
+ * {@code goog.html.SafeStyle.unwrap(style1) +
+ * goog.html.SafeStyle.unwrap(style2)} must itself be a value that satisfies
+ * the SafeStyle type constraint. This requirement implies that for any value
+ * {@code style} of this type, {@code goog.html.SafeStyle.unwrap(style)} must
+ * not end in a "property value" or "property name" context. For example,
+ * a value of {@code background:url("} or {@code font-} would not satisfy the
+ * SafeStyle contract. This is because concatenating such strings with a
+ * second value that itself does not contain unsafe CSS can result in an
+ * overall string that does. For example, if {@code javascript:evil())"} is
+ * appended to {@code background:url("}, the resulting string may result in
+ * the execution of a malicious script.
+ *
+ * TODO(mlourenco): Consider whether we should implement UTF-8 interchange
+ * validity checks and blacklisting of newlines (including Unicode ones) and
+ * other whitespace characters (\t, \f). Document here if so and also update
+ * SafeStyle.fromConstant().
+ *
+ * The following example values comply with this type's contract:
+ * <ul>
+ *   <li><pre>width: 1em;</pre>
+ *   <li><pre>height:1em;</pre>
+ *   <li><pre>width: 1em;height: 1em;</pre>
+ *   <li><pre>background:url('http://url');</pre>
+ * </ul>
+ * In addition, the empty string is safe for use in a CSS attribute.
+ *
+ * The following example values do NOT comply with this type's contract:
+ * <ul>
+ *   <li><pre>background: red</pre> (missing a trailing semi-colon)
+ *   <li><pre>background:</pre> (missing a value and a trailing semi-colon)
+ *   <li><pre>1em</pre> (missing an attribute name, which provides context for
+ *       the value)
+ * </ul>
+ *
+ * @see goog.html.SafeStyle#create
+ * @see goog.html.SafeStyle#fromConstant
+ * @see http://www.w3.org/TR/css3-syntax/
+ * @constructor
+ * @final
+ * @struct
+ * @implements {goog.string.TypedString}
+ */
+goog.html.SafeStyle = function() {
+  /**
+   * The contained value of this SafeStyle.  The field has a purposely
+   * ugly name to make (non-compiled) code that attempts to directly access this
+   * field stand out.
+   * @private {string}
+   */
+  this.privateDoNotAccessOrElseSafeStyleWrappedValue_ = '';
+
+  /**
+   * A type marker used to implement additional run-time type checking.
+   * @see goog.html.SafeStyle#unwrap
+   * @const {!Object}
+   * @private
+   */
+  this.SAFE_STYLE_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ =
+      goog.html.SafeStyle.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_;
+};
+
+
+/**
+ * @override
+ * @const
+ */
+goog.html.SafeStyle.prototype.implementsGoogStringTypedString = true;
+
+
+/**
+ * Type marker for the SafeStyle type, used to implement additional
+ * run-time type checking.
+ * @const {!Object}
+ * @private
+ */
+goog.html.SafeStyle.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {};
+
+
+/**
+ * Creates a SafeStyle object from a compile-time constant string.
+ *
+ * {@code style} should be in the format
+ * {@code name: value; [name: value; ...]} and must not have any < or >
+ * characters in it. This is so that SafeStyle's contract is preserved,
+ * allowing the SafeStyle to correctly be interpreted as a sequence of CSS
+ * declarations and without affecting the syntactic structure of any
+ * surrounding CSS and HTML.
+ *
+ * This method performs basic sanity checks on the format of {@code style}
+ * but does not constrain the format of {@code name} and {@code value}, except
+ * for disallowing tag characters.
+ *
+ * @param {!goog.string.Const} style A compile-time-constant string from which
+ *     to create a SafeStyle.
+ * @return {!goog.html.SafeStyle} A SafeStyle object initialized to
+ *     {@code style}.
+ */
+goog.html.SafeStyle.fromConstant = function(style) {
+  var styleString = goog.string.Const.unwrap(style);
+  if (styleString.length === 0) {
+    return goog.html.SafeStyle.EMPTY;
+  }
+  goog.html.SafeStyle.checkStyle_(styleString);
+  goog.asserts.assert(
+      goog.string.endsWith(styleString, ';'),
+      'Last character of style string is not \';\': ' + styleString);
+  goog.asserts.assert(
+      goog.string.contains(styleString, ':'),
+      'Style string must contain at least one \':\', to ' +
+          'specify a "name: value" pair: ' + styleString);
+  return goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(
+      styleString);
+};
+
+
+/**
+ * Checks if the style definition is valid.
+ * @param {string} style
+ * @private
+ */
+goog.html.SafeStyle.checkStyle_ = function(style) {
+  goog.asserts.assert(
+      !/[<>]/.test(style), 'Forbidden characters in style string: ' + style);
+};
+
+
+/**
+ * Returns this SafeStyle's value as a string.
+ *
+ * IMPORTANT: In code where it is security relevant that an object's type is
+ * indeed {@code SafeStyle}, use {@code goog.html.SafeStyle.unwrap} instead of
+ * this method. If in doubt, assume that it's security relevant. In particular,
+ * note that goog.html functions which return a goog.html type do not guarantee
+ * the returned instance is of the right type. For example:
+ *
+ * <pre>
+ * var fakeSafeHtml = new String('fake');
+ * fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;
+ * var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);
+ * // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by
+ * // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml
+ * // instanceof goog.html.SafeHtml.
+ * </pre>
+ *
+ * @see goog.html.SafeStyle#unwrap
+ * @override
+ */
+goog.html.SafeStyle.prototype.getTypedStringValue = function() {
+  return this.privateDoNotAccessOrElseSafeStyleWrappedValue_;
+};
+
+
+if (goog.DEBUG) {
+  /**
+   * Returns a debug string-representation of this value.
+   *
+   * To obtain the actual string value wrapped in a SafeStyle, use
+   * {@code goog.html.SafeStyle.unwrap}.
+   *
+   * @see goog.html.SafeStyle#unwrap
+   * @override
+   */
+  goog.html.SafeStyle.prototype.toString = function() {
+    return 'SafeStyle{' + this.privateDoNotAccessOrElseSafeStyleWrappedValue_ +
+        '}';
+  };
+}
+
+
+/**
+ * Performs a runtime check that the provided object is indeed a
+ * SafeStyle object, and returns its value.
+ *
+ * @param {!goog.html.SafeStyle} safeStyle The object to extract from.
+ * @return {string} The safeStyle object's contained string, unless
+ *     the run-time type check fails. In that case, {@code unwrap} returns an
+ *     innocuous string, or, if assertions are enabled, throws
+ *     {@code goog.asserts.AssertionError}.
+ */
+goog.html.SafeStyle.unwrap = function(safeStyle) {
+  // Perform additional Run-time type-checking to ensure that
+  // safeStyle is indeed an instance of the expected type.  This
+  // provides some additional protection against security bugs due to
+  // application code that disables type checks.
+  // Specifically, the following checks are performed:
+  // 1. The object is an instance of the expected type.
+  // 2. The object is not an instance of a subclass.
+  // 3. The object carries a type marker for the expected type. "Faking" an
+  // object requires a reference to the type marker, which has names intended
+  // to stand out in code reviews.
+  if (safeStyle instanceof goog.html.SafeStyle &&
+      safeStyle.constructor === goog.html.SafeStyle &&
+      safeStyle.SAFE_STYLE_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ ===
+          goog.html.SafeStyle.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_) {
+    return safeStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
+  } else {
+    goog.asserts.fail('expected object of type SafeStyle, got \'' +
+        safeStyle + '\' of type ' + goog.typeOf(safeStyle));
+    return 'type_error:SafeStyle';
+  }
+};
+
+
+/**
+ * Package-internal utility method to create SafeStyle instances.
+ *
+ * @param {string} style The string to initialize the SafeStyle object with.
+ * @return {!goog.html.SafeStyle} The initialized SafeStyle object.
+ * @package
+ */
+goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse = function(
+    style) {
+  return new goog.html.SafeStyle().initSecurityPrivateDoNotAccessOrElse_(style);
+};
+
+
+/**
+ * Called from createSafeStyleSecurityPrivateDoNotAccessOrElse(). This
+ * method exists only so that the compiler can dead code eliminate static
+ * fields (like EMPTY) when they're not accessed.
+ * @param {string} style
+ * @return {!goog.html.SafeStyle}
+ * @private
+ */
+goog.html.SafeStyle.prototype.initSecurityPrivateDoNotAccessOrElse_ = function(
+    style) {
+  this.privateDoNotAccessOrElseSafeStyleWrappedValue_ = style;
+  return this;
+};
+
+
+/**
+ * A SafeStyle instance corresponding to the empty string.
+ * @const {!goog.html.SafeStyle}
+ */
+goog.html.SafeStyle.EMPTY =
+    goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse('');
+
+
+/**
+ * The innocuous string generated by goog.html.SafeStyle.create when passed
+ * an unsafe value.
+ * @const {string}
+ */
+goog.html.SafeStyle.INNOCUOUS_STRING = 'zClosurez';
+
+
+/**
+ * A single property value.
+ * @typedef {string|!goog.string.Const|!goog.html.SafeUrl}
+ */
+goog.html.SafeStyle.PropertyValue;
+
+
+/**
+ * Mapping of property names to their values.
+ * We don't support numbers even though some values might be numbers (e.g.
+ * line-height or 0 for any length). The reason is that most numeric values need
+ * units (e.g. '1px') and allowing numbers could cause users forgetting about
+ * them.
+ * @typedef {!Object<string, ?goog.html.SafeStyle.PropertyValue|
+ *     ?Array<!goog.html.SafeStyle.PropertyValue>>}
+ */
+goog.html.SafeStyle.PropertyMap;
+
+
+/**
+ * Creates a new SafeStyle object from the properties specified in the map.
+ * @param {goog.html.SafeStyle.PropertyMap} map Mapping of property names to
+ *     their values, for example {'margin': '1px'}. Names must consist of
+ *     [-_a-zA-Z0-9]. Values might be strings consisting of
+ *     [-,.'"%_!# a-zA-Z0-9], where " and ' must be properly balanced. We also
+ *     allow simple functions like rgb() and url() which sanitizes its contents.
+ *     Other values must be wrapped in goog.string.Const. URLs might be passed
+ *     as goog.html.SafeUrl which will be wrapped into url(""). We also support
+ *     array whose elements are joined with ' '. Null value causes skipping the
+ *     property.
+ * @return {!goog.html.SafeStyle}
+ * @throws {Error} If invalid name is provided.
+ * @throws {goog.asserts.AssertionError} If invalid value is provided. With
+ *     disabled assertions, invalid value is replaced by
+ *     goog.html.SafeStyle.INNOCUOUS_STRING.
+ */
+goog.html.SafeStyle.create = function(map) {
+  var style = '';
+  for (var name in map) {
+    if (!/^[-_a-zA-Z0-9]+$/.test(name)) {
+      throw new Error('Name allows only [-_a-zA-Z0-9], got: ' + name);
+    }
+    var value = map[name];
+    if (value == null) {
+      continue;
+    }
+    if (goog.isArray(value)) {
+      value = goog.array.map(value, goog.html.SafeStyle.sanitizePropertyValue_)
+                  .join(' ');
+    } else {
+      value = goog.html.SafeStyle.sanitizePropertyValue_(value);
+    }
+    style += name + ':' + value + ';';
+  }
+  if (!style) {
+    return goog.html.SafeStyle.EMPTY;
+  }
+  goog.html.SafeStyle.checkStyle_(style);
+  return goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(
+      style);
+};
+
+
+/**
+ * Checks and converts value to string.
+ * @param {!goog.html.SafeStyle.PropertyValue} value
+ * @return {string}
+ * @private
+ */
+goog.html.SafeStyle.sanitizePropertyValue_ = function(value) {
+  if (value instanceof goog.html.SafeUrl) {
+    var url = goog.html.SafeUrl.unwrap(value);
+    return 'url("' + url.replace(/</g, '%3c').replace(/[\\"]/g, '\\$&') + '")';
+  }
+  var result = value instanceof goog.string.Const ?
+      goog.string.Const.unwrap(value) :
+      goog.html.SafeStyle.sanitizePropertyValueString_(String(value));
+  // These characters can be used to change context and we don't want that even
+  // with const values.
+  goog.asserts.assert(!/[{;}]/.test(result), 'Value does not allow [{;}].');
+  return result;
+};
+
+
+/**
+ * Checks string value.
+ * @param {string} value
+ * @return {string}
+ * @private
+ */
+goog.html.SafeStyle.sanitizePropertyValueString_ = function(value) {
+  var valueWithoutFunctions =
+      value.replace(goog.html.SafeUrl.FUNCTIONS_RE_, '$1')
+          .replace(goog.html.SafeUrl.URL_RE_, 'url');
+  if (!goog.html.SafeStyle.VALUE_RE_.test(valueWithoutFunctions)) {
+    goog.asserts.fail(
+        'String value allows only ' + goog.html.SafeStyle.VALUE_ALLOWED_CHARS_ +
+        ' and simple functions, got: ' + value);
+    return goog.html.SafeStyle.INNOCUOUS_STRING;
+  } else if (!goog.html.SafeStyle.hasBalancedQuotes_(value)) {
+    goog.asserts.fail('String value requires balanced quotes, got: ' + value);
+    return goog.html.SafeStyle.INNOCUOUS_STRING;
+  }
+  return goog.html.SafeStyle.sanitizeUrl_(value);
+};
+
+
+/**
+ * Checks that quotes (" and ') are properly balanced inside a string. Assumes
+ * that neither escape (\) nor any other character that could result in
+ * breaking out of a string parsing context are allowed;
+ * see http://www.w3.org/TR/css3-syntax/#string-token-diagram.
+ * @param {string} value Untrusted CSS property value.
+ * @return {boolean} True if property value is safe with respect to quote
+ *     balancedness.
+ * @private
+ */
+goog.html.SafeStyle.hasBalancedQuotes_ = function(value) {
+  var outsideSingle = true;
+  var outsideDouble = true;
+  for (var i = 0; i < value.length; i++) {
+    var c = value.charAt(i);
+    if (c == "'" && outsideDouble) {
+      outsideSingle = !outsideSingle;
+    } else if (c == '"' && outsideSingle) {
+      outsideDouble = !outsideDouble;
+    }
+  }
+  return outsideSingle && outsideDouble;
+};
+
+
+/**
+ * Characters allowed in goog.html.SafeStyle.VALUE_RE_.
+ * @private {string}
+ */
+goog.html.SafeStyle.VALUE_ALLOWED_CHARS_ = '[-,."\'%_!# a-zA-Z0-9]';
+
+
+/**
+ * Regular expression for safe values.
+ *
+ * Quotes (" and ') are allowed, but a check must be done elsewhere to ensure
+ * they're balanced.
+ *
+ * ',' allows multiple values to be assigned to the same property
+ * (e.g. background-attachment or font-family) and hence could allow
+ * multiple values to get injected, but that should pose no risk of XSS.
+ *
+ * The expression checks only for XSS safety, not for CSS validity.
+ * @const {!RegExp}
+ * @private
+ */
+goog.html.SafeStyle.VALUE_RE_ =
+    new RegExp('^' + goog.html.SafeStyle.VALUE_ALLOWED_CHARS_ + '+$');
+
+
+/**
+ * Regular expression for url(). We support URLs allowed by
+ * https://www.w3.org/TR/css-syntax-3/#url-token-diagram without using escape
+ * sequences. Use percent-encoding if you need to use special characters like
+ * backslash.
+ * @private @const {!RegExp}
+ */
+goog.html.SafeUrl.URL_RE_ = new RegExp(
+    '\\b(url\\([ \t\n]*)(' +
+        '\'[ -&(-\\[\\]-~]*\'' +  // Printable characters except ' and \.
+        '|"[ !#-\\[\\]-~]*"' +    // Printable characters except " and \.
+        '|[!#-&*-\\[\\]-~]*' +    // Printable characters except [ "'()\\].
+        ')([ \t\n]*\\))',
+    'g');
+
+
+/**
+ * Regular expression for simple functions.
+ * @private @const {!RegExp}
+ */
+goog.html.SafeUrl.FUNCTIONS_RE_ = new RegExp(
+    '\\b(hsl|hsla|rgb|rgba|(rotate|scale|translate)(X|Y|Z|3d)?)' +
+        '\\([-0-9a-z.%, ]+\\)',
+    'g');
+
+
+/**
+ * Sanitize URLs inside url().
+ *
+ * NOTE: We could also consider using CSS.escape once that's available in the
+ * browsers. However, loosely matching URL e.g. with url\(.*\) and then escaping
+ * the contents would result in a slightly different language than CSS leading
+ * to confusion of users. E.g. url(")") is valid in CSS but it would be invalid
+ * as seen by our parser. On the other hand, url(\) is invalid in CSS but our
+ * parser would be fine with it.
+ *
+ * @param {string} value Untrusted CSS property value.
+ * @return {string}
+ * @private
+ */
+goog.html.SafeStyle.sanitizeUrl_ = function(value) {
+  return value.replace(
+      goog.html.SafeUrl.URL_RE_, function(match, before, url, after) {
+        var quote = '';
+        url = url.replace(/^(['"])(.*)\1$/, function(match, start, inside) {
+          quote = start;
+          return inside;
+        });
+        var sanitized = goog.html.SafeUrl.sanitize(url).getTypedStringValue();
+        return before + quote + sanitized + quote + after;
+      });
+};
+
+
+/**
+ * Creates a new SafeStyle object by concatenating the values.
+ * @param {...(!goog.html.SafeStyle|!Array<!goog.html.SafeStyle>)} var_args
+ *     SafeStyles to concatenate.
+ * @return {!goog.html.SafeStyle}
+ */
+goog.html.SafeStyle.concat = function(var_args) {
+  var style = '';
+
+  /**
+   * @param {!goog.html.SafeStyle|!Array<!goog.html.SafeStyle>} argument
+   */
+  var addArgument = function(argument) {
+    if (goog.isArray(argument)) {
+      goog.array.forEach(argument, addArgument);
+    } else {
+      style += goog.html.SafeStyle.unwrap(argument);
+    }
+  };
+
+  goog.array.forEach(arguments, addArgument);
+  if (!style) {
+    return goog.html.SafeStyle.EMPTY;
+  }
+  return goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(
+      style);
+};
diff --git a/third_party/ink/closure/html/safestylesheet.js b/third_party/ink/closure/html/safestylesheet.js
new file mode 100644
index 0000000..f690dbda
--- /dev/null
+++ b/third_party/ink/closure/html/safestylesheet.js
@@ -0,0 +1,344 @@
+// Copyright 2014 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 The SafeStyleSheet type and its builders.
+ *
+ * TODO(xtof): Link to document stating type contract.
+ */
+
+goog.provide('goog.html.SafeStyleSheet');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.html.SafeStyle');
+goog.require('goog.object');
+goog.require('goog.string');
+goog.require('goog.string.Const');
+goog.require('goog.string.TypedString');
+
+
+
+/**
+ * A string-like object which represents a CSS style sheet and that carries the
+ * security type contract that its value, as a string, will not cause untrusted
+ * script execution (XSS) when evaluated as CSS in a browser.
+ *
+ * Instances of this type must be created via the factory method
+ * {@code goog.html.SafeStyleSheet.fromConstant} and not by invoking its
+ * constructor. The constructor intentionally takes no parameters and the type
+ * is immutable; hence only a default instance corresponding to the empty string
+ * can be obtained via constructor invocation.
+ *
+ * A SafeStyleSheet's string representation can safely be interpolated as the
+ * content of a style element within HTML. The SafeStyleSheet string should
+ * not be escaped before interpolation.
+ *
+ * Values of this type must be composable, i.e. for any two values
+ * {@code styleSheet1} and {@code styleSheet2} of this type,
+ * {@code goog.html.SafeStyleSheet.unwrap(styleSheet1) +
+ * goog.html.SafeStyleSheet.unwrap(styleSheet2)} must itself be a value that
+ * satisfies the SafeStyleSheet type constraint. This requirement implies that
+ * for any value {@code styleSheet} of this type,
+ * {@code goog.html.SafeStyleSheet.unwrap(styleSheet1)} must end in
+ * "beginning of rule" context.
+
+ * A SafeStyleSheet can be constructed via security-reviewed unchecked
+ * conversions. In this case producers of SafeStyleSheet must ensure themselves
+ * that the SafeStyleSheet does not contain unsafe script. Note in particular
+ * that {@code &lt;} is dangerous, even when inside CSS strings, and so should
+ * always be forbidden or CSS-escaped in user controlled input. For example, if
+ * {@code &lt;/style&gt;&lt;script&gt;evil&lt;/script&gt;"} were interpolated
+ * inside a CSS string, it would break out of the context of the original
+ * style element and {@code evil} would execute. Also note that within an HTML
+ * style (raw text) element, HTML character references, such as
+ * {@code &amp;lt;}, are not allowed. See
+ *
+ http://www.w3.org/TR/html5/scripting-1.html#restrictions-for-contents-of-script-elements
+ * (similar considerations apply to the style element).
+ *
+ * @see goog.html.SafeStyleSheet#fromConstant
+ * @constructor
+ * @final
+ * @struct
+ * @implements {goog.string.TypedString}
+ */
+goog.html.SafeStyleSheet = function() {
+  /**
+   * The contained value of this SafeStyleSheet.  The field has a purposely
+   * ugly name to make (non-compiled) code that attempts to directly access this
+   * field stand out.
+   * @private {string}
+   */
+  this.privateDoNotAccessOrElseSafeStyleSheetWrappedValue_ = '';
+
+  /**
+   * A type marker used to implement additional run-time type checking.
+   * @see goog.html.SafeStyleSheet#unwrap
+   * @const {!Object}
+   * @private
+   */
+  this.SAFE_STYLE_SHEET_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ =
+      goog.html.SafeStyleSheet.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_;
+};
+
+
+/**
+ * @override
+ * @const
+ */
+goog.html.SafeStyleSheet.prototype.implementsGoogStringTypedString = true;
+
+
+/**
+ * Type marker for the SafeStyleSheet type, used to implement additional
+ * run-time type checking.
+ * @const {!Object}
+ * @private
+ */
+goog.html.SafeStyleSheet.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {};
+
+
+/**
+ * Creates a style sheet consisting of one selector and one style definition.
+ * Use {@link goog.html.SafeStyleSheet.concat} to create longer style sheets.
+ * This function doesn't support @import, @media and similar constructs.
+ * @param {string} selector CSS selector, e.g. '#id' or 'tag .class, #id'. We
+ *     support CSS3 selectors: https://w3.org/TR/css3-selectors/#selectors.
+ * @param {!goog.html.SafeStyle.PropertyMap|!goog.html.SafeStyle} style Style
+ *     definition associated with the selector.
+ * @return {!goog.html.SafeStyleSheet}
+ * @throws {Error} If invalid selector is provided.
+ */
+goog.html.SafeStyleSheet.createRule = function(selector, style) {
+  if (goog.string.contains(selector, '<')) {
+    throw new Error('Selector does not allow \'<\', got: ' + selector);
+  }
+
+  // Remove strings.
+  var selectorToCheck =
+      selector.replace(/('|")((?!\1)[^\r\n\f\\]|\\[\s\S])*\1/g, '');
+
+  // Check characters allowed in CSS3 selectors.
+  if (!/^[-_a-zA-Z0-9#.:* ,>+~[\]()=^$|]+$/.test(selectorToCheck)) {
+    throw new Error(
+        'Selector allows only [-_a-zA-Z0-9#.:* ,>+~[\\]()=^$|] and ' +
+        'strings, got: ' + selector);
+  }
+
+  // Check balanced () and [].
+  if (!goog.html.SafeStyleSheet.hasBalancedBrackets_(selectorToCheck)) {
+    throw new Error('() and [] in selector must be balanced, got: ' + selector);
+  }
+
+  if (!(style instanceof goog.html.SafeStyle)) {
+    style = goog.html.SafeStyle.create(style);
+  }
+  var styleSheet = selector + '{' + goog.html.SafeStyle.unwrap(style) + '}';
+  return goog.html.SafeStyleSheet
+      .createSafeStyleSheetSecurityPrivateDoNotAccessOrElse(styleSheet);
+};
+
+
+/**
+ * Checks if a string has balanced () and [] brackets.
+ * @param {string} s String to check.
+ * @return {boolean}
+ * @private
+ */
+goog.html.SafeStyleSheet.hasBalancedBrackets_ = function(s) {
+  var brackets = {'(': ')', '[': ']'};
+  var expectedBrackets = [];
+  for (var i = 0; i < s.length; i++) {
+    var ch = s[i];
+    if (brackets[ch]) {
+      expectedBrackets.push(brackets[ch]);
+    } else if (goog.object.contains(brackets, ch)) {
+      if (expectedBrackets.pop() != ch) {
+        return false;
+      }
+    }
+  }
+  return expectedBrackets.length == 0;
+};
+
+
+/**
+ * Creates a new SafeStyleSheet object by concatenating values.
+ * @param {...(!goog.html.SafeStyleSheet|!Array<!goog.html.SafeStyleSheet>)}
+ *     var_args Values to concatenate.
+ * @return {!goog.html.SafeStyleSheet}
+ */
+goog.html.SafeStyleSheet.concat = function(var_args) {
+  var result = '';
+
+  /**
+   * @param {!goog.html.SafeStyleSheet|!Array<!goog.html.SafeStyleSheet>}
+   *     argument
+   */
+  var addArgument = function(argument) {
+    if (goog.isArray(argument)) {
+      goog.array.forEach(argument, addArgument);
+    } else {
+      result += goog.html.SafeStyleSheet.unwrap(argument);
+    }
+  };
+
+  goog.array.forEach(arguments, addArgument);
+  return goog.html.SafeStyleSheet
+      .createSafeStyleSheetSecurityPrivateDoNotAccessOrElse(result);
+};
+
+
+/**
+ * Creates a SafeStyleSheet object from a compile-time constant string.
+ *
+ * {@code styleSheet} must not have any &lt; characters in it, so that
+ * the syntactic structure of the surrounding HTML is not affected.
+ *
+ * @param {!goog.string.Const} styleSheet A compile-time-constant string from
+ *     which to create a SafeStyleSheet.
+ * @return {!goog.html.SafeStyleSheet} A SafeStyleSheet object initialized to
+ *     {@code styleSheet}.
+ */
+goog.html.SafeStyleSheet.fromConstant = function(styleSheet) {
+  var styleSheetString = goog.string.Const.unwrap(styleSheet);
+  if (styleSheetString.length === 0) {
+    return goog.html.SafeStyleSheet.EMPTY;
+  }
+  // > is a valid character in CSS selectors and there's no strict need to
+  // block it if we already block <.
+  goog.asserts.assert(
+      !goog.string.contains(styleSheetString, '<'),
+      "Forbidden '<' character in style sheet string: " + styleSheetString);
+  return goog.html.SafeStyleSheet
+      .createSafeStyleSheetSecurityPrivateDoNotAccessOrElse(styleSheetString);
+};
+
+
+/**
+ * Returns this SafeStyleSheet's value as a string.
+ *
+ * IMPORTANT: In code where it is security relevant that an object's type is
+ * indeed {@code SafeStyleSheet}, use {@code goog.html.SafeStyleSheet.unwrap}
+ * instead of this method. If in doubt, assume that it's security relevant. In
+ * particular, note that goog.html functions which return a goog.html type do
+ * not guarantee the returned instance is of the right type. For example:
+ *
+ * <pre>
+ * var fakeSafeHtml = new String('fake');
+ * fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;
+ * var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);
+ * // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by
+ * // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml
+ * // instanceof goog.html.SafeHtml.
+ * </pre>
+ *
+ * @see goog.html.SafeStyleSheet#unwrap
+ * @override
+ */
+goog.html.SafeStyleSheet.prototype.getTypedStringValue = function() {
+  return this.privateDoNotAccessOrElseSafeStyleSheetWrappedValue_;
+};
+
+
+if (goog.DEBUG) {
+  /**
+   * Returns a debug string-representation of this value.
+   *
+   * To obtain the actual string value wrapped in a SafeStyleSheet, use
+   * {@code goog.html.SafeStyleSheet.unwrap}.
+   *
+   * @see goog.html.SafeStyleSheet#unwrap
+   * @override
+   */
+  goog.html.SafeStyleSheet.prototype.toString = function() {
+    return 'SafeStyleSheet{' +
+        this.privateDoNotAccessOrElseSafeStyleSheetWrappedValue_ + '}';
+  };
+}
+
+
+/**
+ * Performs a runtime check that the provided object is indeed a
+ * SafeStyleSheet object, and returns its value.
+ *
+ * @param {!goog.html.SafeStyleSheet} safeStyleSheet The object to extract from.
+ * @return {string} The safeStyleSheet object's contained string, unless
+ *     the run-time type check fails. In that case, {@code unwrap} returns an
+ *     innocuous string, or, if assertions are enabled, throws
+ *     {@code goog.asserts.AssertionError}.
+ */
+goog.html.SafeStyleSheet.unwrap = function(safeStyleSheet) {
+  // Perform additional Run-time type-checking to ensure that
+  // safeStyleSheet is indeed an instance of the expected type.  This
+  // provides some additional protection against security bugs due to
+  // application code that disables type checks.
+  // Specifically, the following checks are performed:
+  // 1. The object is an instance of the expected type.
+  // 2. The object is not an instance of a subclass.
+  // 3. The object carries a type marker for the expected type. "Faking" an
+  // object requires a reference to the type marker, which has names intended
+  // to stand out in code reviews.
+  if (safeStyleSheet instanceof goog.html.SafeStyleSheet &&
+      safeStyleSheet.constructor === goog.html.SafeStyleSheet &&
+      safeStyleSheet
+              .SAFE_STYLE_SHEET_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ ===
+          goog.html.SafeStyleSheet.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_) {
+    return safeStyleSheet.privateDoNotAccessOrElseSafeStyleSheetWrappedValue_;
+  } else {
+    goog.asserts.fail('expected object of type SafeStyleSheet, got \'' +
+        safeStyleSheet + '\' of type ' + goog.typeOf(safeStyleSheet));
+    return 'type_error:SafeStyleSheet';
+  }
+};
+
+
+/**
+ * Package-internal utility method to create SafeStyleSheet instances.
+ *
+ * @param {string} styleSheet The string to initialize the SafeStyleSheet
+ *     object with.
+ * @return {!goog.html.SafeStyleSheet} The initialized SafeStyleSheet object.
+ * @package
+ */
+goog.html.SafeStyleSheet.createSafeStyleSheetSecurityPrivateDoNotAccessOrElse =
+    function(styleSheet) {
+  return new goog.html.SafeStyleSheet().initSecurityPrivateDoNotAccessOrElse_(
+      styleSheet);
+};
+
+
+/**
+ * Called from createSafeStyleSheetSecurityPrivateDoNotAccessOrElse(). This
+ * method exists only so that the compiler can dead code eliminate static
+ * fields (like EMPTY) when they're not accessed.
+ * @param {string} styleSheet
+ * @return {!goog.html.SafeStyleSheet}
+ * @private
+ */
+goog.html.SafeStyleSheet.prototype.initSecurityPrivateDoNotAccessOrElse_ =
+    function(styleSheet) {
+  this.privateDoNotAccessOrElseSafeStyleSheetWrappedValue_ = styleSheet;
+  return this;
+};
+
+
+/**
+ * A SafeStyleSheet instance corresponding to the empty string.
+ * @const {!goog.html.SafeStyleSheet}
+ */
+goog.html.SafeStyleSheet.EMPTY =
+    goog.html.SafeStyleSheet
+        .createSafeStyleSheetSecurityPrivateDoNotAccessOrElse('');
diff --git a/third_party/ink/closure/html/safeurl.js b/third_party/ink/closure/html/safeurl.js
new file mode 100644
index 0000000..3d1ee112
--- /dev/null
+++ b/third_party/ink/closure/html/safeurl.js
@@ -0,0 +1,454 @@
+// Copyright 2013 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 The SafeUrl type and its builders.
+ *
+ * TODO(xtof): Link to document stating type contract.
+ */
+
+goog.provide('goog.html.SafeUrl');
+
+goog.require('goog.asserts');
+goog.require('goog.fs.url');
+goog.require('goog.html.TrustedResourceUrl');
+goog.require('goog.i18n.bidi.Dir');
+goog.require('goog.i18n.bidi.DirectionalString');
+goog.require('goog.string');
+goog.require('goog.string.Const');
+goog.require('goog.string.TypedString');
+
+
+
+/**
+ * A string that is safe to use in URL context in DOM APIs and HTML documents.
+ *
+ * A SafeUrl is a string-like object that carries the security type contract
+ * that its value as a string will not cause untrusted script execution
+ * when evaluated as a hyperlink URL in a browser.
+ *
+ * Values of this type are guaranteed to be safe to use in URL/hyperlink
+ * contexts, such as assignment to URL-valued DOM properties, in the sense that
+ * the use will not result in a Cross-Site-Scripting vulnerability. Similarly,
+ * SafeUrls can be interpolated into the URL context of an HTML template (e.g.,
+ * inside a href attribute). However, appropriate HTML-escaping must still be
+ * applied.
+ *
+ * Note that, as documented in {@code goog.html.SafeUrl.unwrap}, this type's
+ * contract does not guarantee that instances are safe to interpolate into HTML
+ * without appropriate escaping.
+ *
+ * Note also that this type's contract does not imply any guarantees regarding
+ * the resource the URL refers to.  In particular, SafeUrls are <b>not</b>
+ * safe to use in a context where the referred-to resource is interpreted as
+ * trusted code, e.g., as the src of a script tag.
+ *
+ * Instances of this type must be created via the factory methods
+ * ({@code goog.html.SafeUrl.fromConstant}, {@code goog.html.SafeUrl.sanitize}),
+ * etc and not by invoking its constructor.  The constructor intentionally
+ * takes no parameters and the type is immutable; hence only a default instance
+ * corresponding to the empty string can be obtained via constructor invocation.
+ *
+ * @see goog.html.SafeUrl#fromConstant
+ * @see goog.html.SafeUrl#from
+ * @see goog.html.SafeUrl#sanitize
+ * @constructor
+ * @final
+ * @struct
+ * @implements {goog.i18n.bidi.DirectionalString}
+ * @implements {goog.string.TypedString}
+ */
+goog.html.SafeUrl = function() {
+  /**
+   * The contained value of this SafeUrl.  The field has a purposely ugly
+   * name to make (non-compiled) code that attempts to directly access this
+   * field stand out.
+   * @private {string}
+   */
+  this.privateDoNotAccessOrElseSafeHtmlWrappedValue_ = '';
+
+  /**
+   * A type marker used to implement additional run-time type checking.
+   * @see goog.html.SafeUrl#unwrap
+   * @const {!Object}
+   * @private
+   */
+  this.SAFE_URL_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ =
+      goog.html.SafeUrl.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_;
+};
+
+
+/**
+ * The innocuous string generated by goog.html.SafeUrl.sanitize when passed
+ * an unsafe URL.
+ *
+ * about:invalid is registered in
+ * http://www.w3.org/TR/css3-values/#about-invalid.
+ * http://tools.ietf.org/html/rfc6694#section-2.2.1 permits about URLs to
+ * contain a fragment, which is not to be considered when determining if an
+ * about URL is well-known.
+ *
+ * Using about:invalid seems preferable to using a fixed data URL, since
+ * browsers might choose to not report CSP violations on it, as legitimate
+ * CSS function calls to attr() can result in this URL being produced. It is
+ * also a standard URL which matches exactly the semantics we need:
+ * "The about:invalid URI references a non-existent document with a generic
+ * error condition. It can be used when a URI is necessary, but the default
+ * value shouldn't be resolveable as any type of document".
+ *
+ * @const {string}
+ */
+goog.html.SafeUrl.INNOCUOUS_STRING = 'about:invalid#zClosurez';
+
+
+/**
+ * @override
+ * @const
+ */
+goog.html.SafeUrl.prototype.implementsGoogStringTypedString = true;
+
+
+/**
+ * Returns this SafeUrl's value a string.
+ *
+ * IMPORTANT: In code where it is security relevant that an object's type is
+ * indeed {@code SafeUrl}, use {@code goog.html.SafeUrl.unwrap} instead of this
+ * method. If in doubt, assume that it's security relevant. In particular, note
+ * that goog.html functions which return a goog.html type do not guarantee that
+ * the returned instance is of the right type. For example:
+ *
+ * <pre>
+ * var fakeSafeHtml = new String('fake');
+ * fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;
+ * var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);
+ * // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by
+ * // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml instanceof
+ * // goog.html.SafeHtml.
+ * </pre>
+ *
+ * IMPORTANT: The guarantees of the SafeUrl type contract only extend to the
+ * behavior of browsers when interpreting URLs. Values of SafeUrl objects MUST
+ * be appropriately escaped before embedding in a HTML document. Note that the
+ * required escaping is context-sensitive (e.g. a different escaping is
+ * required for embedding a URL in a style property within a style
+ * attribute, as opposed to embedding in a href attribute).
+ *
+ * @see goog.html.SafeUrl#unwrap
+ * @override
+ */
+goog.html.SafeUrl.prototype.getTypedStringValue = function() {
+  return this.privateDoNotAccessOrElseSafeHtmlWrappedValue_;
+};
+
+
+/**
+ * @override
+ * @const
+ */
+goog.html.SafeUrl.prototype.implementsGoogI18nBidiDirectionalString = true;
+
+
+/**
+ * Returns this URLs directionality, which is always {@code LTR}.
+ * @override
+ */
+goog.html.SafeUrl.prototype.getDirection = function() {
+  return goog.i18n.bidi.Dir.LTR;
+};
+
+
+if (goog.DEBUG) {
+  /**
+   * Returns a debug string-representation of this value.
+   *
+   * To obtain the actual string value wrapped in a SafeUrl, use
+   * {@code goog.html.SafeUrl.unwrap}.
+   *
+   * @see goog.html.SafeUrl#unwrap
+   * @override
+   */
+  goog.html.SafeUrl.prototype.toString = function() {
+    return 'SafeUrl{' + this.privateDoNotAccessOrElseSafeHtmlWrappedValue_ +
+        '}';
+  };
+}
+
+
+/**
+ * Performs a runtime check that the provided object is indeed a SafeUrl
+ * object, and returns its value.
+ *
+ * IMPORTANT: The guarantees of the SafeUrl type contract only extend to the
+ * behavior of  browsers when interpreting URLs. Values of SafeUrl objects MUST
+ * be appropriately escaped before embedding in a HTML document. Note that the
+ * required escaping is context-sensitive (e.g. a different escaping is
+ * required for embedding a URL in a style property within a style
+ * attribute, as opposed to embedding in a href attribute).
+ *
+ * @param {!goog.html.SafeUrl} safeUrl The object to extract from.
+ * @return {string} The SafeUrl object's contained string, unless the run-time
+ *     type check fails. In that case, {@code unwrap} returns an innocuous
+ *     string, or, if assertions are enabled, throws
+ *     {@code goog.asserts.AssertionError}.
+ */
+goog.html.SafeUrl.unwrap = function(safeUrl) {
+  // Perform additional Run-time type-checking to ensure that safeUrl is indeed
+  // an instance of the expected type.  This provides some additional protection
+  // against security bugs due to application code that disables type checks.
+  // Specifically, the following checks are performed:
+  // 1. The object is an instance of the expected type.
+  // 2. The object is not an instance of a subclass.
+  // 3. The object carries a type marker for the expected type. "Faking" an
+  // object requires a reference to the type marker, which has names intended
+  // to stand out in code reviews.
+  if (safeUrl instanceof goog.html.SafeUrl &&
+      safeUrl.constructor === goog.html.SafeUrl &&
+      safeUrl.SAFE_URL_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ ===
+          goog.html.SafeUrl.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_) {
+    return safeUrl.privateDoNotAccessOrElseSafeHtmlWrappedValue_;
+  } else {
+    goog.asserts.fail('expected object of type SafeUrl, got \'' +
+        safeUrl + '\' of type ' + goog.typeOf(safeUrl));
+    return 'type_error:SafeUrl';
+  }
+};
+
+
+/**
+ * Creates a SafeUrl object from a compile-time constant string.
+ *
+ * Compile-time constant strings are inherently program-controlled and hence
+ * trusted.
+ *
+ * @param {!goog.string.Const} url A compile-time-constant string from which to
+ *         create a SafeUrl.
+ * @return {!goog.html.SafeUrl} A SafeUrl object initialized to {@code url}.
+ */
+goog.html.SafeUrl.fromConstant = function(url) {
+  return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(
+      goog.string.Const.unwrap(url));
+};
+
+
+/**
+ * A pattern that matches Blob or data types that can have SafeUrls created
+ * from URL.createObjectURL(blob) or via a data: URI.
+ * @const
+ * @private
+ */
+goog.html.SAFE_MIME_TYPE_PATTERN_ = new RegExp(
+    '^(?:audio/(?:3gpp|3gpp2|aac|midi|mp4|mpeg|ogg|x-m4a|x-wav|webm)|' +
+        'image/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|' +
+        'text/csv|' +
+        'video/(?:mpeg|mp4|ogg|webm))$',
+    'i');
+
+
+/**
+ * Creates a SafeUrl wrapping a blob URL for the given {@code blob}.
+ *
+ * The blob URL is created with {@code URL.createObjectURL}. If the MIME type
+ * for {@code blob} is not of a known safe audio, image or video MIME type,
+ * then the SafeUrl will wrap {@link #INNOCUOUS_STRING}.
+ *
+ * @see http://www.w3.org/TR/FileAPI/#url
+ * @param {!Blob} blob
+ * @return {!goog.html.SafeUrl} The blob URL, or an innocuous string wrapped
+ *   as a SafeUrl.
+ */
+goog.html.SafeUrl.fromBlob = function(blob) {
+  var url = goog.html.SAFE_MIME_TYPE_PATTERN_.test(blob.type) ?
+      goog.fs.url.createObjectUrl(blob) :
+      goog.html.SafeUrl.INNOCUOUS_STRING;
+  return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(url);
+};
+
+
+/**
+ * Matches a base-64 data URL, with the first match group being the MIME type.
+ * @const
+ * @private
+ */
+goog.html.DATA_URL_PATTERN_ = /^data:([^;,]*);base64,[a-z0-9+\/]+=*$/i;
+
+
+/**
+ * Creates a SafeUrl wrapping a data: URL, after validating it matches a
+ * known-safe audio, image or video MIME type.
+ *
+ * @param {string} dataUrl A valid base64 data URL with one of the whitelisted
+ *     audio, image or video MIME types.
+ * @return {!goog.html.SafeUrl} A matching safe URL, or {@link INNOCUOUS_STRING}
+ *     wrapped as a SafeUrl if it does not pass.
+ */
+goog.html.SafeUrl.fromDataUrl = function(dataUrl) {
+  // There's a slight risk here that a browser sniffs the content type if it
+  // doesn't know the MIME type and executes HTML within the data: URL. For this
+  // to cause XSS it would also have to execute the HTML in the same origin
+  // of the page with the link. It seems unlikely that both of these will
+  // happen, particularly in not really old IEs.
+  var match = dataUrl.match(goog.html.DATA_URL_PATTERN_);
+  var valid = match && goog.html.SAFE_MIME_TYPE_PATTERN_.test(match[1]);
+  return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(
+      valid ? dataUrl : goog.html.SafeUrl.INNOCUOUS_STRING);
+};
+
+
+/**
+ * Creates a SafeUrl wrapping a tel: URL.
+ *
+ * @param {string} telUrl A tel URL.
+ * @return {!goog.html.SafeUrl} A matching safe URL, or {@link INNOCUOUS_STRING}
+ *     wrapped as a SafeUrl if it does not pass.
+ */
+goog.html.SafeUrl.fromTelUrl = function(telUrl) {
+  // There's a risk that a tel: URL could immediately place a call once
+  // clicked, without requiring user confirmation. For that reason it is
+  // handled in this separate function.
+  if (!goog.string.caseInsensitiveStartsWith(telUrl, 'tel:')) {
+    telUrl = goog.html.SafeUrl.INNOCUOUS_STRING;
+  }
+  return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(
+      telUrl);
+};
+
+
+/**
+ * Creates a SafeUrl from TrustedResourceUrl. This is safe because
+ * TrustedResourceUrl is more tightly restricted than SafeUrl.
+ *
+ * @param {!goog.html.TrustedResourceUrl} trustedResourceUrl
+ * @return {!goog.html.SafeUrl}
+ */
+goog.html.SafeUrl.fromTrustedResourceUrl = function(trustedResourceUrl) {
+  return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(
+      goog.html.TrustedResourceUrl.unwrap(trustedResourceUrl));
+};
+
+
+/**
+ * A pattern that recognizes a commonly useful subset of URLs that satisfy
+ * the SafeUrl contract.
+ *
+ * This regular expression matches a subset of URLs that will not cause script
+ * execution if used in URL context within a HTML document. Specifically, this
+ * regular expression matches if (comment from here on and regex copied from
+ * Soy's EscapingConventions):
+ * (1) Either a protocol in a whitelist (http, https, mailto or ftp).
+ * (2) or no protocol.  A protocol must be followed by a colon. The below
+ *     allows that by allowing colons only after one of the characters [/?#].
+ *     A colon after a hash (#) must be in the fragment.
+ *     Otherwise, a colon after a (?) must be in a query.
+ *     Otherwise, a colon after a single solidus (/) must be in a path.
+ *     Otherwise, a colon after a double solidus (//) must be in the authority
+ *     (before port).
+ *
+ * @private
+ * @const {!RegExp}
+ */
+goog.html.SAFE_URL_PATTERN_ =
+    /^(?:(?:https?|mailto|ftp):|[^:/?#]*(?:[/?#]|$))/i;
+
+
+/**
+ * Creates a SafeUrl object from {@code url}. If {@code url} is a
+ * goog.html.SafeUrl then it is simply returned. Otherwise the input string is
+ * validated to match a pattern of commonly used safe URLs.
+ *
+ * {@code url} may be a URL with the http, https, mailto or ftp scheme,
+ * or a relative URL (i.e., a URL without a scheme; specifically, a
+ * scheme-relative, absolute-path-relative, or path-relative URL).
+ *
+ * @see http://url.spec.whatwg.org/#concept-relative-url
+ * @param {string|!goog.string.TypedString} url The URL to validate.
+ * @return {!goog.html.SafeUrl} The validated URL, wrapped as a SafeUrl.
+ */
+goog.html.SafeUrl.sanitize = function(url) {
+  if (url instanceof goog.html.SafeUrl) {
+    return url;
+  } else if (url.implementsGoogStringTypedString) {
+    url = url.getTypedStringValue();
+  } else {
+    url = String(url);
+  }
+  if (!goog.html.SAFE_URL_PATTERN_.test(url)) {
+    url = goog.html.SafeUrl.INNOCUOUS_STRING;
+  }
+  return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(url);
+};
+
+/**
+ * Creates a SafeUrl object from {@code url}. If {@code url} is a
+ * goog.html.SafeUrl then it is simply returned. Otherwise the input string is
+ * validated to match a pattern of commonly used safe URLs.
+ *
+ * {@code url} may be a URL with the http, https, mailto or ftp scheme,
+ * or a relative URL (i.e., a URL without a scheme; specifically, a
+ * scheme-relative, absolute-path-relative, or path-relative URL).
+ *
+ * This function asserts (using goog.asserts) that the URL matches this pattern.
+ * If it does not, in addition to failing the assert, an innocous URL will be
+ * returned.
+ *
+ * @see http://url.spec.whatwg.org/#concept-relative-url
+ * @param {string|!goog.string.TypedString} url The URL to validate.
+ * @return {!goog.html.SafeUrl} The validated URL, wrapped as a SafeUrl.
+ */
+goog.html.SafeUrl.sanitizeAssertUnchanged = function(url) {
+  if (url instanceof goog.html.SafeUrl) {
+    return url;
+  } else if (url.implementsGoogStringTypedString) {
+    url = url.getTypedStringValue();
+  } else {
+    url = String(url);
+  }
+  if (!goog.asserts.assert(goog.html.SAFE_URL_PATTERN_.test(url))) {
+    url = goog.html.SafeUrl.INNOCUOUS_STRING;
+  }
+  return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(url);
+};
+
+
+
+/**
+ * Type marker for the SafeUrl type, used to implement additional run-time
+ * type checking.
+ * @const {!Object}
+ * @private
+ */
+goog.html.SafeUrl.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {};
+
+
+/**
+ * Package-internal utility method to create SafeUrl instances.
+ *
+ * @param {string} url The string to initialize the SafeUrl object with.
+ * @return {!goog.html.SafeUrl} The initialized SafeUrl object.
+ * @package
+ */
+goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse = function(
+    url) {
+  var safeUrl = new goog.html.SafeUrl();
+  safeUrl.privateDoNotAccessOrElseSafeHtmlWrappedValue_ = url;
+  return safeUrl;
+};
+
+
+/**
+ * A SafeUrl corresponding to the special about:blank url.
+ * @const {!goog.html.SafeUrl}
+ */
+goog.html.SafeUrl.ABOUT_BLANK =
+    goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(
+        'about:blank');
diff --git a/third_party/ink/closure/html/trustedresourceurl.js b/third_party/ink/closure/html/trustedresourceurl.js
new file mode 100644
index 0000000..b7a67bc
--- /dev/null
+++ b/third_party/ink/closure/html/trustedresourceurl.js
@@ -0,0 +1,412 @@
+// Copyright 2013 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 The TrustedResourceUrl type and its builders.
+ *
+ * TODO(xtof): Link to document stating type contract.
+ */
+
+goog.provide('goog.html.TrustedResourceUrl');
+
+goog.require('goog.asserts');
+goog.require('goog.i18n.bidi.Dir');
+goog.require('goog.i18n.bidi.DirectionalString');
+goog.require('goog.string.Const');
+goog.require('goog.string.TypedString');
+
+
+
+/**
+ * A URL which is under application control and from which script, CSS, and
+ * other resources that represent executable code, can be fetched.
+ *
+ * Given that the URL can only be constructed from strings under application
+ * control and is used to load resources, bugs resulting in a malformed URL
+ * should not have a security impact and are likely to be easily detectable
+ * during testing. Given the wide number of non-RFC compliant URLs in use,
+ * stricter validation could prevent some applications from being able to use
+ * this type.
+ *
+ * Instances of this type must be created via the factory method,
+ * ({@code fromConstant}, {@code fromConstants}, {@code format} or {@code
+ * formatWithParams}), and not by invoking its constructor. The constructor
+ * intentionally takes no parameters and the type is immutable; hence only a
+ * default instance corresponding to the empty string can be obtained via
+ * constructor invocation.
+ *
+ * @see goog.html.TrustedResourceUrl#fromConstant
+ * @constructor
+ * @final
+ * @struct
+ * @implements {goog.i18n.bidi.DirectionalString}
+ * @implements {goog.string.TypedString}
+ */
+goog.html.TrustedResourceUrl = function() {
+  /**
+   * The contained value of this TrustedResourceUrl.  The field has a purposely
+   * ugly name to make (non-compiled) code that attempts to directly access this
+   * field stand out.
+   * @private {string}
+   */
+  this.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue_ = '';
+
+  /**
+   * A type marker used to implement additional run-time type checking.
+   * @see goog.html.TrustedResourceUrl#unwrap
+   * @const {!Object}
+   * @private
+   */
+  this.TRUSTED_RESOURCE_URL_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ =
+      goog.html.TrustedResourceUrl.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_;
+};
+
+
+/**
+ * @override
+ * @const
+ */
+goog.html.TrustedResourceUrl.prototype.implementsGoogStringTypedString = true;
+
+
+/**
+ * Returns this TrustedResourceUrl's value as a string.
+ *
+ * IMPORTANT: In code where it is security relevant that an object's type is
+ * indeed {@code TrustedResourceUrl}, use
+ * {@code goog.html.TrustedResourceUrl.unwrap} instead of this method. If in
+ * doubt, assume that it's security relevant. In particular, note that
+ * goog.html functions which return a goog.html type do not guarantee that
+ * the returned instance is of the right type. For example:
+ *
+ * <pre>
+ * var fakeSafeHtml = new String('fake');
+ * fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;
+ * var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);
+ * // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by
+ * // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml instanceof
+ * // goog.html.SafeHtml.
+ * </pre>
+ *
+ * @see goog.html.TrustedResourceUrl#unwrap
+ * @override
+ */
+goog.html.TrustedResourceUrl.prototype.getTypedStringValue = function() {
+  return this.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue_;
+};
+
+
+/**
+ * @override
+ * @const
+ */
+goog.html.TrustedResourceUrl.prototype.implementsGoogI18nBidiDirectionalString =
+    true;
+
+
+/**
+ * Returns this URLs directionality, which is always {@code LTR}.
+ * @override
+ */
+goog.html.TrustedResourceUrl.prototype.getDirection = function() {
+  return goog.i18n.bidi.Dir.LTR;
+};
+
+
+if (goog.DEBUG) {
+  /**
+   * Returns a debug string-representation of this value.
+   *
+   * To obtain the actual string value wrapped in a TrustedResourceUrl, use
+   * {@code goog.html.TrustedResourceUrl.unwrap}.
+   *
+   * @see goog.html.TrustedResourceUrl#unwrap
+   * @override
+   */
+  goog.html.TrustedResourceUrl.prototype.toString = function() {
+    return 'TrustedResourceUrl{' +
+        this.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue_ + '}';
+  };
+}
+
+
+/**
+ * Performs a runtime check that the provided object is indeed a
+ * TrustedResourceUrl object, and returns its value.
+ *
+ * @param {!goog.html.TrustedResourceUrl} trustedResourceUrl The object to
+ *     extract from.
+ * @return {string} The trustedResourceUrl object's contained string, unless
+ *     the run-time type check fails. In that case, {@code unwrap} returns an
+ *     innocuous string, or, if assertions are enabled, throws
+ *     {@code goog.asserts.AssertionError}.
+ */
+goog.html.TrustedResourceUrl.unwrap = function(trustedResourceUrl) {
+  // Perform additional Run-time type-checking to ensure that
+  // trustedResourceUrl is indeed an instance of the expected type.  This
+  // provides some additional protection against security bugs due to
+  // application code that disables type checks.
+  // Specifically, the following checks are performed:
+  // 1. The object is an instance of the expected type.
+  // 2. The object is not an instance of a subclass.
+  // 3. The object carries a type marker for the expected type. "Faking" an
+  // object requires a reference to the type marker, which has names intended
+  // to stand out in code reviews.
+  if (trustedResourceUrl instanceof goog.html.TrustedResourceUrl &&
+      trustedResourceUrl.constructor === goog.html.TrustedResourceUrl &&
+      trustedResourceUrl
+              .TRUSTED_RESOURCE_URL_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ ===
+          goog.html.TrustedResourceUrl
+              .TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_) {
+    return trustedResourceUrl
+        .privateDoNotAccessOrElseTrustedResourceUrlWrappedValue_;
+  } else {
+    goog.asserts.fail('expected object of type TrustedResourceUrl, got \'' +
+        trustedResourceUrl + '\' of type ' + goog.typeOf(trustedResourceUrl));
+    return 'type_error:TrustedResourceUrl';
+  }
+};
+
+
+/**
+ * Creates a TrustedResourceUrl from a format string and arguments.
+ *
+ * The arguments for interpolation into the format string map labels to values.
+ * Values of type `goog.string.Const` are interpolated without modifcation.
+ * Values of other types are cast to string and encoded with
+ * encodeURIComponent.
+ *
+ * `%{<label>}` markers are used in the format string to indicate locations
+ * to be interpolated with the valued mapped to the given label. `<label>`
+ * must contain only alphanumeric and `_` characters.
+ *
+ * The format string must start with one of the following:
+ * - `https://<origin>/`
+ * - `//<origin>/`
+ * - `/<pathStart>`
+ * - `about:blank`
+ *
+ * `<origin>` must contain only alphanumeric or any of the following: `-.:[]`.
+ * `<pathStart>` is any character except `/` and `\`.
+ *
+ * Example usage:
+ *
+ *    var url = goog.html.TrustedResourceUrl.format(goog.string.Const.from(
+ *        'https://www.google.com/search?q=%{query}), {'query': searchTerm});
+ *
+ *    var url = goog.html.TrustedResourceUrl.format(goog.string.Const.from(
+ *        '//www.youtube.com/v/%{videoId}?hl=en&fs=1%{autoplay}'), {
+ *        'videoId': videoId,
+ *        'autoplay': opt_autoplay ?
+ *            goog.string.Const.from('&autoplay=1') : goog.string.Const.EMPTY
+ *    });
+ *
+ * While this function can be used to create a TrustedResourceUrl from only
+ * constants, fromConstant() and fromConstants() are generally preferable for
+ * that purpose.
+ *
+ * @param {!goog.string.Const} format The format string.
+ * @param {!Object<string, (string|number|!goog.string.Const)>} args Mapping
+ *     of labels to values to be interpolated into the format string.
+ *     goog.string.Const values are interpolated without encoding.
+ * @return {!goog.html.TrustedResourceUrl}
+ * @throws {!Error} On an invalid format string or if a label used in the
+ *     the format string is not present in args.
+ */
+goog.html.TrustedResourceUrl.format = function(format, args) {
+  var result = goog.html.TrustedResourceUrl.format_(format, args);
+  return goog.html.TrustedResourceUrl
+      .createTrustedResourceUrlSecurityPrivateDoNotAccessOrElse(result);
+};
+
+
+/**
+ * String version of TrustedResourceUrl.format.
+ * @param {!goog.string.Const} format
+ * @param {!Object<string, (string|number|!goog.string.Const)>} args
+ * @return {string}
+ * @throws {!Error}
+ * @private
+ */
+goog.html.TrustedResourceUrl.format_ = function(format, args) {
+  var formatStr = goog.string.Const.unwrap(format);
+  if (!goog.html.TrustedResourceUrl.BASE_URL_.test(formatStr)) {
+    throw new Error('Invalid TrustedResourceUrl format: ' + formatStr);
+  }
+  return formatStr.replace(
+      goog.html.TrustedResourceUrl.FORMAT_MARKER_, function(match, id) {
+        if (!Object.prototype.hasOwnProperty.call(args, id)) {
+          throw new Error(
+              'Found marker, "' + id + '", in format string, "' + formatStr +
+              '", but no valid label mapping found ' +
+              'in args: ' + JSON.stringify(args));
+        }
+        var arg = args[id];
+        if (arg instanceof goog.string.Const) {
+          return goog.string.Const.unwrap(arg);
+        } else {
+          return encodeURIComponent(String(arg));
+        }
+      });
+};
+
+
+/**
+ * @private @const {!RegExp}
+ */
+goog.html.TrustedResourceUrl.FORMAT_MARKER_ = /%{(\w+)}/g;
+
+
+/**
+ * The URL must be absolute, scheme-relative or path-absolute. So it must
+ * start with:
+ * - https:// followed by allowed origin characters.
+ * - // followed by allowed origin characters.
+ * - / not followed by / or \. There will only be an absolute path.
+ *
+ * Based on
+ * https://url.spec.whatwg.org/commit-snapshots/56b74ce7cca8883eab62e9a12666e2fac665d03d/#url-parsing
+ * an initial / which is not followed by another / or \ will end up in the "path
+ * state" and from there it can only go to "fragment state" and "query state".
+ *
+ * We don't enforce a well-formed domain name. So '.' or '1.2' are valid.
+ * That's ok because the origin comes from a compile-time constant.
+ *
+ * A regular expression is used instead of goog.uri for several reasons:
+ * - Strictness. E.g. we don't want any userinfo component and we don't
+ *   want '/./, nor \' in the first path component.
+ * - Small trusted base. goog.uri is generic and might need to change,
+ *   reasoning about all the ways it can parse a URL now and in the future
+ *   is error-prone.
+ * - Code size. We expect many calls to .format(), many of which might
+ *   not be using goog.uri.
+ * - Simplicity. Using goog.uri would likely not result in simpler nor shorter
+ *   code.
+ * @private @const {!RegExp}
+ */
+goog.html.TrustedResourceUrl.BASE_URL_ =
+    /^(?:https:)?\/\/[0-9a-z.:[\]-]+\/|^\/[^\/\\]|^about:blank(#|$)/i;
+
+
+/**
+ * Formats the URL same as TrustedResourceUrl.format and then adds extra URL
+ * parameters.
+ *
+ * Example usage:
+ *
+ *     // Creates '//www.youtube.com/v/abc?autoplay=1' for videoId='abc' and
+ *     // opt_autoplay=1. Creates '//www.youtube.com/v/abc' for videoId='abc'
+ *     // and opt_autoplay=undefined.
+ *     var url = goog.html.TrustedResourceUrl.formatWithParams(
+ *         goog.string.Const.from('//www.youtube.com/v/%{videoId}'),
+ *         {'videoId': videoId},
+ *         {'autoplay': opt_autoplay});
+ *
+ * @param {!goog.string.Const} format The format string.
+ * @param {!Object<string, (string|number|!goog.string.Const)>} args Mapping
+ *     of labels to values to be interpolated into the format string.
+ *     goog.string.Const values are interpolated without encoding.
+ * @param {!Object<string, *>} params Parameters to add to URL. Parameters with
+ *     value {@code null} or {@code undefined} are skipped. Both keys and values
+ *     are encoded. If the value is an array then the same parameter is added
+ *     for every element in the array. Note that JavaScript doesn't guarantee
+ *     the order of values in an object which might result in non-deterministic
+ *     order of the parameters. However, browsers currently preserve the order.
+ * @return {!goog.html.TrustedResourceUrl}
+ * @throws {!Error} On an invalid format string or if a label used in the
+ *     the format string is not present in args.
+ */
+goog.html.TrustedResourceUrl.formatWithParams = function(format, args, params) {
+  var url = goog.html.TrustedResourceUrl.format_(format, args);
+  var separator = /\?/.test(url) ? '&' : '?';
+  for (var key in params) {
+    var values = goog.isArray(params[key]) ? params[key] : [params[key]];
+    for (var i = 0; i < values.length; i++) {
+      if (values[i] == null) {
+        continue;
+      }
+      url += separator + encodeURIComponent(key) + '=' +
+          encodeURIComponent(String(values[i]));
+      separator = '&';
+    }
+  }
+  return goog.html.TrustedResourceUrl
+      .createTrustedResourceUrlSecurityPrivateDoNotAccessOrElse(url);
+};
+
+
+/**
+ * Creates a TrustedResourceUrl object from a compile-time constant string.
+ *
+ * Compile-time constant strings are inherently program-controlled and hence
+ * trusted.
+ *
+ * @param {!goog.string.Const} url A compile-time-constant string from which to
+ *     create a TrustedResourceUrl.
+ * @return {!goog.html.TrustedResourceUrl} A TrustedResourceUrl object
+ *     initialized to {@code url}.
+ */
+goog.html.TrustedResourceUrl.fromConstant = function(url) {
+  return goog.html.TrustedResourceUrl
+      .createTrustedResourceUrlSecurityPrivateDoNotAccessOrElse(
+          goog.string.Const.unwrap(url));
+};
+
+
+/**
+ * Creates a TrustedResourceUrl object from a compile-time constant strings.
+ *
+ * Compile-time constant strings are inherently program-controlled and hence
+ * trusted.
+ *
+ * @param {!Array<!goog.string.Const>} parts Compile-time-constant strings from
+ *     which to create a TrustedResourceUrl.
+ * @return {!goog.html.TrustedResourceUrl} A TrustedResourceUrl object
+ *     initialized to concatenation of {@code parts}.
+ */
+goog.html.TrustedResourceUrl.fromConstants = function(parts) {
+  var unwrapped = '';
+  for (var i = 0; i < parts.length; i++) {
+    unwrapped += goog.string.Const.unwrap(parts[i]);
+  }
+  return goog.html.TrustedResourceUrl
+      .createTrustedResourceUrlSecurityPrivateDoNotAccessOrElse(unwrapped);
+};
+
+
+/**
+ * Type marker for the TrustedResourceUrl type, used to implement additional
+ * run-time type checking.
+ * @const {!Object}
+ * @private
+ */
+goog.html.TrustedResourceUrl.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {};
+
+
+/**
+ * Package-internal utility method to create TrustedResourceUrl instances.
+ *
+ * @param {string} url The string to initialize the TrustedResourceUrl object
+ *     with.
+ * @return {!goog.html.TrustedResourceUrl} The initialized TrustedResourceUrl
+ *     object.
+ * @package
+ */
+goog.html.TrustedResourceUrl
+    .createTrustedResourceUrlSecurityPrivateDoNotAccessOrElse = function(url) {
+  var trustedResourceUrl = new goog.html.TrustedResourceUrl();
+  trustedResourceUrl.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue_ =
+      url;
+  return trustedResourceUrl;
+};
diff --git a/third_party/ink/closure/html/uncheckedconversions.js b/third_party/ink/closure/html/uncheckedconversions.js
new file mode 100644
index 0000000..e75dfa2
--- /dev/null
+++ b/third_party/ink/closure/html/uncheckedconversions.js
@@ -0,0 +1,254 @@
+// Copyright 2013 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 Unchecked conversions to create values of goog.html types from
+ * plain strings.  Use of these functions could potentially result in instances
+ * of goog.html types that violate their type contracts, and hence result in
+ * security vulnerabilties.
+ *
+ * Therefore, all uses of the methods herein must be carefully security
+ * reviewed.  Avoid use of the methods in this file whenever possible; instead
+ * prefer to create instances of goog.html types using inherently safe builders
+ * or template systems.
+ *
+ * MOE:begin_intracomment_strip
+ * See http://go/safehtml-unchecked for guidelines on using these functions.
+ * MOE:end_intracomment_strip
+ *
+ * MOE:begin_intracomment_strip
+ * MAINTAINERS: Use of these functions is detected with a Tricorder analyzer.
+ * If adding functions here also add them to analyzer's list at
+ * j/c/g/devtools/staticanalysis/pipeline/analyzers/shared/SafeHtmlAnalyzers.java.
+ * MOE:end_intracomment_strip
+ *
+ * @visibility {//javascript/closure/html:approved_for_unchecked_conversion}
+ * @visibility {//javascript/closure/bin/sizetests:__pkg__}
+ */
+
+
+goog.provide('goog.html.uncheckedconversions');
+
+goog.require('goog.asserts');
+goog.require('goog.html.SafeHtml');
+goog.require('goog.html.SafeScript');
+goog.require('goog.html.SafeStyle');
+goog.require('goog.html.SafeStyleSheet');
+goog.require('goog.html.SafeUrl');
+goog.require('goog.html.TrustedResourceUrl');
+goog.require('goog.string');
+goog.require('goog.string.Const');
+
+
+/**
+ * Performs an "unchecked conversion" to SafeHtml from a plain string that is
+ * known to satisfy the SafeHtml type contract.
+ *
+ * IMPORTANT: Uses of this method must be carefully security-reviewed to ensure
+ * that the value of {@code html} satisfies the SafeHtml type contract in all
+ * possible program states.
+ *
+ * MOE:begin_intracomment_strip
+ * See http://go/safehtml-unchecked for guidelines on using these functions.
+ * MOE:end_intracomment_strip
+ *
+ * @param {!goog.string.Const} justification A constant string explaining why
+ *     this use of this method is safe. May include a security review ticket
+ *     number.
+ * @param {string} html A string that is claimed to adhere to the SafeHtml
+ *     contract.
+ * @param {?goog.i18n.bidi.Dir=} opt_dir The optional directionality of the
+ *     SafeHtml to be constructed. A null or undefined value signifies an
+ *     unknown directionality.
+ * @return {!goog.html.SafeHtml} The value of html, wrapped in a SafeHtml
+ *     object.
+ */
+goog.html.uncheckedconversions.safeHtmlFromStringKnownToSatisfyTypeContract =
+    function(justification, html, opt_dir) {
+  // unwrap() called inside an assert so that justification can be optimized
+  // away in production code.
+  goog.asserts.assertString(
+      goog.string.Const.unwrap(justification), 'must provide justification');
+  goog.asserts.assert(
+      !goog.string.isEmptyOrWhitespace(goog.string.Const.unwrap(justification)),
+      'must provide non-empty justification');
+  return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
+      html, opt_dir || null);
+};
+
+
+/**
+ * Performs an "unchecked conversion" to SafeScript from a plain string that is
+ * known to satisfy the SafeScript type contract.
+ *
+ * IMPORTANT: Uses of this method must be carefully security-reviewed to ensure
+ * that the value of {@code script} satisfies the SafeScript type contract in
+ * all possible program states.
+ *
+ * MOE:begin_intracomment_strip
+ * See http://go/safehtml-unchecked for guidelines on using these functions.
+ * MOE:end_intracomment_strip
+ *
+ * @param {!goog.string.Const} justification A constant string explaining why
+ *     this use of this method is safe. May include a security review ticket
+ *     number.
+ * @param {string} script The string to wrap as a SafeScript.
+ * @return {!goog.html.SafeScript} The value of {@code script}, wrapped in a
+ *     SafeScript object.
+ */
+goog.html.uncheckedconversions.safeScriptFromStringKnownToSatisfyTypeContract =
+    function(justification, script) {
+  // unwrap() called inside an assert so that justification can be optimized
+  // away in production code.
+  goog.asserts.assertString(
+      goog.string.Const.unwrap(justification), 'must provide justification');
+  goog.asserts.assert(
+      !goog.string.isEmptyOrWhitespace(goog.string.Const.unwrap(justification)),
+      'must provide non-empty justification');
+  return goog.html.SafeScript.createSafeScriptSecurityPrivateDoNotAccessOrElse(
+      script);
+};
+
+
+/**
+ * Performs an "unchecked conversion" to SafeStyle from a plain string that is
+ * known to satisfy the SafeStyle type contract.
+ *
+ * IMPORTANT: Uses of this method must be carefully security-reviewed to ensure
+ * that the value of {@code style} satisfies the SafeStyle type contract in all
+ * possible program states.
+ *
+ * MOE:begin_intracomment_strip
+ * See http://go/safehtml-unchecked for guidelines on using these functions.
+ * MOE:end_intracomment_strip
+ *
+ * @param {!goog.string.Const} justification A constant string explaining why
+ *     this use of this method is safe. May include a security review ticket
+ *     number.
+ * @param {string} style The string to wrap as a SafeStyle.
+ * @return {!goog.html.SafeStyle} The value of {@code style}, wrapped in a
+ *     SafeStyle object.
+ */
+goog.html.uncheckedconversions.safeStyleFromStringKnownToSatisfyTypeContract =
+    function(justification, style) {
+  // unwrap() called inside an assert so that justification can be optimized
+  // away in production code.
+  goog.asserts.assertString(
+      goog.string.Const.unwrap(justification), 'must provide justification');
+  goog.asserts.assert(
+      !goog.string.isEmptyOrWhitespace(goog.string.Const.unwrap(justification)),
+      'must provide non-empty justification');
+  return goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(
+      style);
+};
+
+
+/**
+ * Performs an "unchecked conversion" to SafeStyleSheet from a plain string
+ * that is known to satisfy the SafeStyleSheet type contract.
+ *
+ * IMPORTANT: Uses of this method must be carefully security-reviewed to ensure
+ * that the value of {@code styleSheet} satisfies the SafeStyleSheet type
+ * contract in all possible program states.
+ *
+ * MOE:begin_intracomment_strip
+ * See http://go/safehtml-unchecked for guidelines on using these functions.
+ * MOE:end_intracomment_strip
+ *
+ * @param {!goog.string.Const} justification A constant string explaining why
+ *     this use of this method is safe. May include a security review ticket
+ *     number.
+ * @param {string} styleSheet The string to wrap as a SafeStyleSheet.
+ * @return {!goog.html.SafeStyleSheet} The value of {@code styleSheet}, wrapped
+ *     in a SafeStyleSheet object.
+ */
+goog.html.uncheckedconversions
+    .safeStyleSheetFromStringKnownToSatisfyTypeContract = function(
+    justification, styleSheet) {
+  // unwrap() called inside an assert so that justification can be optimized
+  // away in production code.
+  goog.asserts.assertString(
+      goog.string.Const.unwrap(justification), 'must provide justification');
+  goog.asserts.assert(
+      !goog.string.isEmptyOrWhitespace(goog.string.Const.unwrap(justification)),
+      'must provide non-empty justification');
+  return goog.html.SafeStyleSheet
+      .createSafeStyleSheetSecurityPrivateDoNotAccessOrElse(styleSheet);
+};
+
+
+/**
+ * Performs an "unchecked conversion" to SafeUrl from a plain string that is
+ * known to satisfy the SafeUrl type contract.
+ *
+ * IMPORTANT: Uses of this method must be carefully security-reviewed to ensure
+ * that the value of {@code url} satisfies the SafeUrl type contract in all
+ * possible program states.
+ *
+ * MOE:begin_intracomment_strip
+ * See http://go/safehtml-unchecked for guidelines on using these functions.
+ * MOE:end_intracomment_strip
+ *
+ * @param {!goog.string.Const} justification A constant string explaining why
+ *     this use of this method is safe. May include a security review ticket
+ *     number.
+ * @param {string} url The string to wrap as a SafeUrl.
+ * @return {!goog.html.SafeUrl} The value of {@code url}, wrapped in a SafeUrl
+ *     object.
+ */
+goog.html.uncheckedconversions.safeUrlFromStringKnownToSatisfyTypeContract =
+    function(justification, url) {
+  // unwrap() called inside an assert so that justification can be optimized
+  // away in production code.
+  goog.asserts.assertString(
+      goog.string.Const.unwrap(justification), 'must provide justification');
+  goog.asserts.assert(
+      !goog.string.isEmptyOrWhitespace(goog.string.Const.unwrap(justification)),
+      'must provide non-empty justification');
+  return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(url);
+};
+
+
+/**
+ * Performs an "unchecked conversion" to TrustedResourceUrl from a plain string
+ * that is known to satisfy the TrustedResourceUrl type contract.
+ *
+ * IMPORTANT: Uses of this method must be carefully security-reviewed to ensure
+ * that the value of {@code url} satisfies the TrustedResourceUrl type contract
+ * in all possible program states.
+ *
+ * MOE:begin_intracomment_strip
+ * See http://go/safehtml-unchecked for guidelines on using these functions.
+ * MOE:end_intracomment_strip
+ *
+ * @param {!goog.string.Const} justification A constant string explaining why
+ *     this use of this method is safe. May include a security review ticket
+ *     number.
+ * @param {string} url The string to wrap as a TrustedResourceUrl.
+ * @return {!goog.html.TrustedResourceUrl} The value of {@code url}, wrapped in
+ *     a TrustedResourceUrl object.
+ */
+goog.html.uncheckedconversions
+    .trustedResourceUrlFromStringKnownToSatisfyTypeContract = function(
+    justification, url) {
+  // unwrap() called inside an assert so that justification can be optimized
+  // away in production code.
+  goog.asserts.assertString(
+      goog.string.Const.unwrap(justification), 'must provide justification');
+  goog.asserts.assert(
+      !goog.string.isEmptyOrWhitespace(goog.string.Const.unwrap(justification)),
+      'must provide non-empty justification');
+  return goog.html.TrustedResourceUrl
+      .createTrustedResourceUrlSecurityPrivateDoNotAccessOrElse(url);
+};
diff --git a/third_party/ink/closure/i18n/bidi.js b/third_party/ink/closure/i18n/bidi.js
new file mode 100644
index 0000000..fcf1120
--- /dev/null
+++ b/third_party/ink/closure/i18n/bidi.js
@@ -0,0 +1,878 @@
+// Copyright 2007 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 Utility functions for supporting Bidi issues.
+ * @author shanjian@google.com (Shanjian Li)
+ * @author dougfelt@google.com (Doug Felt)
+ */
+
+
+/**
+ * Namespace for bidi supporting functions.
+ */
+goog.provide('goog.i18n.bidi');
+goog.provide('goog.i18n.bidi.Dir');
+goog.provide('goog.i18n.bidi.DirectionalString');
+goog.provide('goog.i18n.bidi.Format');
+
+
+/**
+ * @define {boolean} FORCE_RTL forces the {@link goog.i18n.bidi.IS_RTL} constant
+ * to say that the current locale is a RTL locale.  This should only be used
+ * if you want to override the default behavior for deciding whether the
+ * current locale is RTL or not.
+ *
+ * {@see goog.i18n.bidi.IS_RTL}
+ */
+goog.define('goog.i18n.bidi.FORCE_RTL', false);
+
+
+/**
+ * Constant that defines whether or not the current locale is a RTL locale.
+ * If {@link goog.i18n.bidi.FORCE_RTL} is not true, this constant will default
+ * to check that {@link goog.LOCALE} is one of a few major RTL locales.
+ *
+ * <p>This is designed to be a maximally efficient compile-time constant. For
+ * example, for the default goog.LOCALE, compiling
+ * "if (goog.i18n.bidi.IS_RTL) alert('rtl') else {}" should produce no code. It
+ * is this design consideration that limits the implementation to only
+ * supporting a few major RTL locales, as opposed to the broader repertoire of
+ * something like goog.i18n.bidi.isRtlLanguage.
+ *
+ * <p>Since this constant refers to the directionality of the locale, it is up
+ * to the caller to determine if this constant should also be used for the
+ * direction of the UI.
+ *
+ * {@see goog.LOCALE}
+ *
+ * @type {boolean}
+ *
+ * TODO(aharon): write a test that checks that this is a compile-time constant.
+ */
+goog.i18n.bidi.IS_RTL = goog.i18n.bidi.FORCE_RTL ||
+    ((goog.LOCALE.substring(0, 2).toLowerCase() == 'ar' ||
+      goog.LOCALE.substring(0, 2).toLowerCase() == 'fa' ||
+      goog.LOCALE.substring(0, 2).toLowerCase() == 'he' ||
+      goog.LOCALE.substring(0, 2).toLowerCase() == 'iw' ||
+      goog.LOCALE.substring(0, 2).toLowerCase() == 'ps' ||
+      goog.LOCALE.substring(0, 2).toLowerCase() == 'sd' ||
+      goog.LOCALE.substring(0, 2).toLowerCase() == 'ug' ||
+      goog.LOCALE.substring(0, 2).toLowerCase() == 'ur' ||
+      goog.LOCALE.substring(0, 2).toLowerCase() == 'yi') &&
+     (goog.LOCALE.length == 2 || goog.LOCALE.substring(2, 3) == '-' ||
+      goog.LOCALE.substring(2, 3) == '_')) ||
+    (goog.LOCALE.length >= 3 &&
+     goog.LOCALE.substring(0, 3).toLowerCase() == 'ckb' &&
+     (goog.LOCALE.length == 3 || goog.LOCALE.substring(3, 4) == '-' ||
+      goog.LOCALE.substring(3, 4) == '_'));
+
+
+/**
+ * Unicode formatting characters and directionality string constants.
+ * @enum {string}
+ */
+goog.i18n.bidi.Format = {
+  /** Unicode "Left-To-Right Embedding" (LRE) character. */
+  LRE: '\u202A',
+  /** Unicode "Right-To-Left Embedding" (RLE) character. */
+  RLE: '\u202B',
+  /** Unicode "Pop Directional Formatting" (PDF) character. */
+  PDF: '\u202C',
+  /** Unicode "Left-To-Right Mark" (LRM) character. */
+  LRM: '\u200E',
+  /** Unicode "Right-To-Left Mark" (RLM) character. */
+  RLM: '\u200F'
+};
+
+
+/**
+ * Directionality enum.
+ * @enum {number}
+ */
+goog.i18n.bidi.Dir = {
+  /**
+   * Left-to-right.
+   */
+  LTR: 1,
+
+  /**
+   * Right-to-left.
+   */
+  RTL: -1,
+
+  /**
+   * Neither left-to-right nor right-to-left.
+   */
+  NEUTRAL: 0
+};
+
+
+/**
+ * 'right' string constant.
+ * @type {string}
+ */
+goog.i18n.bidi.RIGHT = 'right';
+
+
+/**
+ * 'left' string constant.
+ * @type {string}
+ */
+goog.i18n.bidi.LEFT = 'left';
+
+
+/**
+ * 'left' if locale is RTL, 'right' if not.
+ * @type {string}
+ */
+goog.i18n.bidi.I18N_RIGHT =
+    goog.i18n.bidi.IS_RTL ? goog.i18n.bidi.LEFT : goog.i18n.bidi.RIGHT;
+
+
+/**
+ * 'right' if locale is RTL, 'left' if not.
+ * @type {string}
+ */
+goog.i18n.bidi.I18N_LEFT =
+    goog.i18n.bidi.IS_RTL ? goog.i18n.bidi.RIGHT : goog.i18n.bidi.LEFT;
+
+
+/**
+ * Convert a directionality given in various formats to a goog.i18n.bidi.Dir
+ * constant. Useful for interaction with different standards of directionality
+ * representation.
+ *
+ * @param {goog.i18n.bidi.Dir|number|boolean|null} givenDir Directionality given
+ *     in one of the following formats:
+ *     1. A goog.i18n.bidi.Dir constant.
+ *     2. A number (positive = LTR, negative = RTL, 0 = neutral).
+ *     3. A boolean (true = RTL, false = LTR).
+ *     4. A null for unknown directionality.
+ * @param {boolean=} opt_noNeutral Whether a givenDir of zero or
+ *     goog.i18n.bidi.Dir.NEUTRAL should be treated as null, i.e. unknown, in
+ *     order to preserve legacy behavior.
+ * @return {?goog.i18n.bidi.Dir} A goog.i18n.bidi.Dir constant matching the
+ *     given directionality. If given null, returns null (i.e. unknown).
+ */
+goog.i18n.bidi.toDir = function(givenDir, opt_noNeutral) {
+  if (typeof givenDir == 'number') {
+    // This includes the non-null goog.i18n.bidi.Dir case.
+    return givenDir > 0 ? goog.i18n.bidi.Dir.LTR : givenDir < 0 ?
+                          goog.i18n.bidi.Dir.RTL :
+                          opt_noNeutral ? null : goog.i18n.bidi.Dir.NEUTRAL;
+  } else if (givenDir == null) {
+    return null;
+  } else {
+    // Must be typeof givenDir == 'boolean'.
+    return givenDir ? goog.i18n.bidi.Dir.RTL : goog.i18n.bidi.Dir.LTR;
+  }
+};
+
+
+/**
+ * A practical pattern to identify strong LTR characters. This pattern is not
+ * theoretically correct according to the Unicode standard. It is simplified for
+ * performance and small code size.
+ * @type {string}
+ * @private
+ */
+goog.i18n.bidi.ltrChars_ =
+    'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF' +
+    '\u200E\u2C00-\uFB1C\uFE00-\uFE6F\uFEFD-\uFFFF';
+
+
+/**
+ * A practical pattern to identify strong RTL character. This pattern is not
+ * theoretically correct according to the Unicode standard. It is simplified
+ * for performance and small code size.
+ * @type {string}
+ * @private
+ */
+goog.i18n.bidi.rtlChars_ =
+    '\u0591-\u06EF\u06FA-\u07FF\u200F\uFB1D-\uFDFF\uFE70-\uFEFC';
+
+
+/**
+ * Simplified regular expression for an HTML tag (opening or closing) or an HTML
+ * escape. We might want to skip over such expressions when estimating the text
+ * directionality.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.htmlSkipReg_ = /<[^>]*>|&[^;]+;/g;
+
+
+/**
+ * Returns the input text with spaces instead of HTML tags or HTML escapes, if
+ * opt_isStripNeeded is true. Else returns the input as is.
+ * Useful for text directionality estimation.
+ * Note: the function should not be used in other contexts; it is not 100%
+ * correct, but rather a good-enough implementation for directionality
+ * estimation purposes.
+ * @param {string} str The given string.
+ * @param {boolean=} opt_isStripNeeded Whether to perform the stripping.
+ *     Default: false (to retain consistency with calling functions).
+ * @return {string} The given string cleaned of HTML tags / escapes.
+ * @private
+ */
+goog.i18n.bidi.stripHtmlIfNeeded_ = function(str, opt_isStripNeeded) {
+  return opt_isStripNeeded ? str.replace(goog.i18n.bidi.htmlSkipReg_, '') : str;
+};
+
+
+/**
+ * Regular expression to check for RTL characters.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.rtlCharReg_ = new RegExp('[' + goog.i18n.bidi.rtlChars_ + ']');
+
+
+/**
+ * Regular expression to check for LTR characters.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.ltrCharReg_ = new RegExp('[' + goog.i18n.bidi.ltrChars_ + ']');
+
+
+/**
+ * Test whether the given string has any RTL characters in it.
+ * @param {string} str The given string that need to be tested.
+ * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {boolean} Whether the string contains RTL characters.
+ */
+goog.i18n.bidi.hasAnyRtl = function(str, opt_isHtml) {
+  return goog.i18n.bidi.rtlCharReg_.test(
+      goog.i18n.bidi.stripHtmlIfNeeded_(str, opt_isHtml));
+};
+
+
+/**
+ * Test whether the given string has any RTL characters in it.
+ * @param {string} str The given string that need to be tested.
+ * @return {boolean} Whether the string contains RTL characters.
+ * @deprecated Use hasAnyRtl.
+ */
+goog.i18n.bidi.hasRtlChar = goog.i18n.bidi.hasAnyRtl;
+
+
+/**
+ * Test whether the given string has any LTR characters in it.
+ * @param {string} str The given string that need to be tested.
+ * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {boolean} Whether the string contains LTR characters.
+ */
+goog.i18n.bidi.hasAnyLtr = function(str, opt_isHtml) {
+  return goog.i18n.bidi.ltrCharReg_.test(
+      goog.i18n.bidi.stripHtmlIfNeeded_(str, opt_isHtml));
+};
+
+
+/**
+ * Regular expression pattern to check if the first character in the string
+ * is LTR.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.ltrRe_ = new RegExp('^[' + goog.i18n.bidi.ltrChars_ + ']');
+
+
+/**
+ * Regular expression pattern to check if the first character in the string
+ * is RTL.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.rtlRe_ = new RegExp('^[' + goog.i18n.bidi.rtlChars_ + ']');
+
+
+/**
+ * Check if the first character in the string is RTL or not.
+ * @param {string} str The given string that need to be tested.
+ * @return {boolean} Whether the first character in str is an RTL char.
+ */
+goog.i18n.bidi.isRtlChar = function(str) {
+  return goog.i18n.bidi.rtlRe_.test(str);
+};
+
+
+/**
+ * Check if the first character in the string is LTR or not.
+ * @param {string} str The given string that need to be tested.
+ * @return {boolean} Whether the first character in str is an LTR char.
+ */
+goog.i18n.bidi.isLtrChar = function(str) {
+  return goog.i18n.bidi.ltrRe_.test(str);
+};
+
+
+/**
+ * Check if the first character in the string is neutral or not.
+ * @param {string} str The given string that need to be tested.
+ * @return {boolean} Whether the first character in str is a neutral char.
+ */
+goog.i18n.bidi.isNeutralChar = function(str) {
+  return !goog.i18n.bidi.isLtrChar(str) && !goog.i18n.bidi.isRtlChar(str);
+};
+
+
+/**
+ * Regular expressions to check if a piece of text is of LTR directionality
+ * on first character with strong directionality.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.ltrDirCheckRe_ = new RegExp(
+    '^[^' + goog.i18n.bidi.rtlChars_ + ']*[' + goog.i18n.bidi.ltrChars_ + ']');
+
+
+/**
+ * Regular expressions to check if a piece of text is of RTL directionality
+ * on first character with strong directionality.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.rtlDirCheckRe_ = new RegExp(
+    '^[^' + goog.i18n.bidi.ltrChars_ + ']*[' + goog.i18n.bidi.rtlChars_ + ']');
+
+
+/**
+ * Check whether the first strongly directional character (if any) is RTL.
+ * @param {string} str String being checked.
+ * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {boolean} Whether RTL directionality is detected using the first
+ *     strongly-directional character method.
+ */
+goog.i18n.bidi.startsWithRtl = function(str, opt_isHtml) {
+  return goog.i18n.bidi.rtlDirCheckRe_.test(
+      goog.i18n.bidi.stripHtmlIfNeeded_(str, opt_isHtml));
+};
+
+
+/**
+ * Check whether the first strongly directional character (if any) is RTL.
+ * @param {string} str String being checked.
+ * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {boolean} Whether RTL directionality is detected using the first
+ *     strongly-directional character method.
+ * @deprecated Use startsWithRtl.
+ */
+goog.i18n.bidi.isRtlText = goog.i18n.bidi.startsWithRtl;
+
+
+/**
+ * Check whether the first strongly directional character (if any) is LTR.
+ * @param {string} str String being checked.
+ * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {boolean} Whether LTR directionality is detected using the first
+ *     strongly-directional character method.
+ */
+goog.i18n.bidi.startsWithLtr = function(str, opt_isHtml) {
+  return goog.i18n.bidi.ltrDirCheckRe_.test(
+      goog.i18n.bidi.stripHtmlIfNeeded_(str, opt_isHtml));
+};
+
+
+/**
+ * Check whether the first strongly directional character (if any) is LTR.
+ * @param {string} str String being checked.
+ * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {boolean} Whether LTR directionality is detected using the first
+ *     strongly-directional character method.
+ * @deprecated Use startsWithLtr.
+ */
+goog.i18n.bidi.isLtrText = goog.i18n.bidi.startsWithLtr;
+
+
+/**
+ * Regular expression to check if a string looks like something that must
+ * always be LTR even in RTL text, e.g. a URL. When estimating the
+ * directionality of text containing these, we treat these as weakly LTR,
+ * like numbers.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.isRequiredLtrRe_ = /^http:\/\/.*/;
+
+
+/**
+ * Check whether the input string either contains no strongly directional
+ * characters or looks like a url.
+ * @param {string} str String being checked.
+ * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {boolean} Whether neutral directionality is detected.
+ */
+goog.i18n.bidi.isNeutralText = function(str, opt_isHtml) {
+  str = goog.i18n.bidi.stripHtmlIfNeeded_(str, opt_isHtml);
+  return goog.i18n.bidi.isRequiredLtrRe_.test(str) ||
+      !goog.i18n.bidi.hasAnyLtr(str) && !goog.i18n.bidi.hasAnyRtl(str);
+};
+
+
+/**
+ * Regular expressions to check if the last strongly-directional character in a
+ * piece of text is LTR.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.ltrExitDirCheckRe_ = new RegExp(
+    '[' + goog.i18n.bidi.ltrChars_ + '][^' + goog.i18n.bidi.rtlChars_ + ']*$');
+
+
+/**
+ * Regular expressions to check if the last strongly-directional character in a
+ * piece of text is RTL.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.rtlExitDirCheckRe_ = new RegExp(
+    '[' + goog.i18n.bidi.rtlChars_ + '][^' + goog.i18n.bidi.ltrChars_ + ']*$');
+
+
+/**
+ * Check if the exit directionality a piece of text is LTR, i.e. if the last
+ * strongly-directional character in the string is LTR.
+ * @param {string} str String being checked.
+ * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {boolean} Whether LTR exit directionality was detected.
+ */
+goog.i18n.bidi.endsWithLtr = function(str, opt_isHtml) {
+  return goog.i18n.bidi.ltrExitDirCheckRe_.test(
+      goog.i18n.bidi.stripHtmlIfNeeded_(str, opt_isHtml));
+};
+
+
+/**
+ * Check if the exit directionality a piece of text is LTR, i.e. if the last
+ * strongly-directional character in the string is LTR.
+ * @param {string} str String being checked.
+ * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {boolean} Whether LTR exit directionality was detected.
+ * @deprecated Use endsWithLtr.
+ */
+goog.i18n.bidi.isLtrExitText = goog.i18n.bidi.endsWithLtr;
+
+
+/**
+ * Check if the exit directionality a piece of text is RTL, i.e. if the last
+ * strongly-directional character in the string is RTL.
+ * @param {string} str String being checked.
+ * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {boolean} Whether RTL exit directionality was detected.
+ */
+goog.i18n.bidi.endsWithRtl = function(str, opt_isHtml) {
+  return goog.i18n.bidi.rtlExitDirCheckRe_.test(
+      goog.i18n.bidi.stripHtmlIfNeeded_(str, opt_isHtml));
+};
+
+
+/**
+ * Check if the exit directionality a piece of text is RTL, i.e. if the last
+ * strongly-directional character in the string is RTL.
+ * @param {string} str String being checked.
+ * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {boolean} Whether RTL exit directionality was detected.
+ * @deprecated Use endsWithRtl.
+ */
+goog.i18n.bidi.isRtlExitText = goog.i18n.bidi.endsWithRtl;
+
+
+/**
+ * A regular expression for matching right-to-left language codes.
+ * See {@link #isRtlLanguage} for the design.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.rtlLocalesRe_ = new RegExp(
+    '^(ar|ckb|dv|he|iw|fa|nqo|ps|sd|ug|ur|yi|' +
+        '.*[-_](Arab|Hebr|Thaa|Nkoo|Tfng))' +
+        '(?!.*[-_](Latn|Cyrl)($|-|_))($|-|_)',
+    'i');
+
+
+/**
+ * Check if a BCP 47 / III language code indicates an RTL language, i.e. either:
+ * - a language code explicitly specifying one of the right-to-left scripts,
+ *   e.g. "az-Arab", or<p>
+ * - a language code specifying one of the languages normally written in a
+ *   right-to-left script, e.g. "fa" (Farsi), except ones explicitly specifying
+ *   Latin or Cyrillic script (which are the usual LTR alternatives).<p>
+ * The list of right-to-left scripts appears in the 100-199 range in
+ * http://www.unicode.org/iso15924/iso15924-num.html, of which Arabic and
+ * Hebrew are by far the most widely used. We also recognize Thaana, N'Ko, and
+ * Tifinagh, which also have significant modern usage. The rest (Syriac,
+ * Samaritan, Mandaic, etc.) seem to have extremely limited or no modern usage
+ * and are not recognized to save on code size.
+ * The languages usually written in a right-to-left script are taken as those
+ * with Suppress-Script: Hebr|Arab|Thaa|Nkoo|Tfng  in
+ * http://www.iana.org/assignments/language-subtag-registry,
+ * as well as Central (or Sorani) Kurdish (ckb), Sindhi (sd) and Uyghur (ug).
+ * Other subtags of the language code, e.g. regions like EG (Egypt), are
+ * ignored.
+ * @param {string} lang BCP 47 (a.k.a III) language code.
+ * @return {boolean} Whether the language code is an RTL language.
+ */
+goog.i18n.bidi.isRtlLanguage = function(lang) {
+  return goog.i18n.bidi.rtlLocalesRe_.test(lang);
+};
+
+
+/**
+ * Regular expression for bracket guard replacement in text.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.bracketGuardTextRe_ =
+    /(\(.*?\)+)|(\[.*?\]+)|(\{.*?\}+)|(<.*?>+)/g;
+
+
+/**
+ * Apply bracket guard using LRM and RLM. This is to address the problem of
+ * messy bracket display frequently happens in RTL layout.
+ * This function works for plain text, not for HTML. In HTML, the opening
+ * bracket might be in a different context than the closing bracket (such as
+ * an attribute value).
+ * @param {string} s The string that need to be processed.
+ * @param {boolean=} opt_isRtlContext specifies default direction (usually
+ *     direction of the UI).
+ * @return {string} The processed string, with all bracket guarded.
+ */
+goog.i18n.bidi.guardBracketInText = function(s, opt_isRtlContext) {
+  var useRtl = opt_isRtlContext === undefined ? goog.i18n.bidi.hasAnyRtl(s) :
+                                                opt_isRtlContext;
+  var mark = useRtl ? goog.i18n.bidi.Format.RLM : goog.i18n.bidi.Format.LRM;
+  return s.replace(goog.i18n.bidi.bracketGuardTextRe_, mark + '$&' + mark);
+};
+
+
+/**
+ * Enforce the html snippet in RTL directionality regardless overall context.
+ * If the html piece was enclosed by tag, dir will be applied to existing
+ * tag, otherwise a span tag will be added as wrapper. For this reason, if
+ * html snippet start with with tag, this tag must enclose the whole piece. If
+ * the tag already has a dir specified, this new one will override existing
+ * one in behavior (tested on FF and IE).
+ * @param {string} html The string that need to be processed.
+ * @return {string} The processed string, with directionality enforced to RTL.
+ */
+goog.i18n.bidi.enforceRtlInHtml = function(html) {
+  if (html.charAt(0) == '<') {
+    return html.replace(/<\w+/, '$& dir=rtl');
+  }
+  // '\n' is important for FF so that it won't incorrectly merge span groups
+  return '\n<span dir=rtl>' + html + '</span>';
+};
+
+
+/**
+ * Enforce RTL on both end of the given text piece using unicode BiDi formatting
+ * characters RLE and PDF.
+ * @param {string} text The piece of text that need to be wrapped.
+ * @return {string} The wrapped string after process.
+ */
+goog.i18n.bidi.enforceRtlInText = function(text) {
+  return goog.i18n.bidi.Format.RLE + text + goog.i18n.bidi.Format.PDF;
+};
+
+
+/**
+ * Enforce the html snippet in RTL directionality regardless overall context.
+ * If the html piece was enclosed by tag, dir will be applied to existing
+ * tag, otherwise a span tag will be added as wrapper. For this reason, if
+ * html snippet start with with tag, this tag must enclose the whole piece. If
+ * the tag already has a dir specified, this new one will override existing
+ * one in behavior (tested on FF and IE).
+ * @param {string} html The string that need to be processed.
+ * @return {string} The processed string, with directionality enforced to RTL.
+ */
+goog.i18n.bidi.enforceLtrInHtml = function(html) {
+  if (html.charAt(0) == '<') {
+    return html.replace(/<\w+/, '$& dir=ltr');
+  }
+  // '\n' is important for FF so that it won't incorrectly merge span groups
+  return '\n<span dir=ltr>' + html + '</span>';
+};
+
+
+/**
+ * Enforce LTR on both end of the given text piece using unicode BiDi formatting
+ * characters LRE and PDF.
+ * @param {string} text The piece of text that need to be wrapped.
+ * @return {string} The wrapped string after process.
+ */
+goog.i18n.bidi.enforceLtrInText = function(text) {
+  return goog.i18n.bidi.Format.LRE + text + goog.i18n.bidi.Format.PDF;
+};
+
+
+/**
+ * Regular expression to find dimensions such as "padding: .3 0.4ex 5px 6;"
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.dimensionsRe_ =
+    /:\s*([.\d][.\w]*)\s+([.\d][.\w]*)\s+([.\d][.\w]*)\s+([.\d][.\w]*)/g;
+
+
+/**
+ * Regular expression for left.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.leftRe_ = /left/gi;
+
+
+/**
+ * Regular expression for right.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.rightRe_ = /right/gi;
+
+
+/**
+ * Placeholder regular expression for swapping.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.tempRe_ = /%%%%/g;
+
+
+/**
+ * Swap location parameters and 'left'/'right' in CSS specification. The
+ * processed string will be suited for RTL layout. Though this function can
+ * cover most cases, there are always exceptions. It is suggested to put
+ * those exceptions in separate group of CSS string.
+ * @param {string} cssStr CSS spefication string.
+ * @return {string} Processed CSS specification string.
+ */
+goog.i18n.bidi.mirrorCSS = function(cssStr) {
+  return cssStr
+      .
+      // reverse dimensions
+      replace(goog.i18n.bidi.dimensionsRe_, ':$1 $4 $3 $2')
+      .replace(goog.i18n.bidi.leftRe_, '%%%%')
+      .  // swap left and right
+      replace(goog.i18n.bidi.rightRe_, goog.i18n.bidi.LEFT)
+      .replace(goog.i18n.bidi.tempRe_, goog.i18n.bidi.RIGHT);
+};
+
+
+/**
+ * Regular expression for hebrew double quote substitution, finding quote
+ * directly after hebrew characters.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.doubleQuoteSubstituteRe_ = /([\u0591-\u05f2])"/g;
+
+
+/**
+ * Regular expression for hebrew single quote substitution, finding quote
+ * directly after hebrew characters.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.singleQuoteSubstituteRe_ = /([\u0591-\u05f2])'/g;
+
+
+/**
+ * Replace the double and single quote directly after a Hebrew character with
+ * GERESH and GERSHAYIM. In such case, most likely that's user intention.
+ * @param {string} str String that need to be processed.
+ * @return {string} Processed string with double/single quote replaced.
+ */
+goog.i18n.bidi.normalizeHebrewQuote = function(str) {
+  return str.replace(goog.i18n.bidi.doubleQuoteSubstituteRe_, '$1\u05f4')
+      .replace(goog.i18n.bidi.singleQuoteSubstituteRe_, '$1\u05f3');
+};
+
+
+/**
+ * Regular expression to split a string into "words" for directionality
+ * estimation based on relative word counts.
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.wordSeparatorRe_ = /\s+/;
+
+
+/**
+ * Regular expression to check if a string contains any numerals. Used to
+ * differentiate between completely neutral strings and those containing
+ * numbers, which are weakly LTR.
+ *
+ * Native Arabic digits (\u0660 - \u0669) are not included because although they
+ * do flow left-to-right inside a number, this is the case even if the  overall
+ * directionality is RTL, and a mathematical expression using these digits is
+ * supposed to flow right-to-left overall, including unary plus and minus
+ * appearing to the right of a number, and this does depend on the overall
+ * directionality being RTL. The digits used in Farsi (\u06F0 - \u06F9), on the
+ * other hand, are included, since Farsi math (including unary plus and minus)
+ * does flow left-to-right.
+ *
+ * @type {RegExp}
+ * @private
+ */
+goog.i18n.bidi.hasNumeralsRe_ = /[\d\u06f0-\u06f9]/;
+
+
+/**
+ * This constant controls threshold of RTL directionality.
+ * @type {number}
+ * @private
+ */
+goog.i18n.bidi.rtlDetectionThreshold_ = 0.40;
+
+
+/**
+ * Estimates the directionality of a string based on relative word counts.
+ * If the number of RTL words is above a certain percentage of the total number
+ * of strongly directional words, returns RTL.
+ * Otherwise, if any words are strongly or weakly LTR, returns LTR.
+ * Otherwise, returns UNKNOWN, which is used to mean "neutral".
+ * Numbers are counted as weakly LTR.
+ * @param {string} str The string to be checked.
+ * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {goog.i18n.bidi.Dir} Estimated overall directionality of {@code str}.
+ */
+goog.i18n.bidi.estimateDirection = function(str, opt_isHtml) {
+  var rtlCount = 0;
+  var totalCount = 0;
+  var hasWeaklyLtr = false;
+  var tokens = goog.i18n.bidi.stripHtmlIfNeeded_(str, opt_isHtml)
+                   .split(goog.i18n.bidi.wordSeparatorRe_);
+  for (var i = 0; i < tokens.length; i++) {
+    var token = tokens[i];
+    if (goog.i18n.bidi.startsWithRtl(token)) {
+      rtlCount++;
+      totalCount++;
+    } else if (goog.i18n.bidi.isRequiredLtrRe_.test(token)) {
+      hasWeaklyLtr = true;
+    } else if (goog.i18n.bidi.hasAnyLtr(token)) {
+      totalCount++;
+    } else if (goog.i18n.bidi.hasNumeralsRe_.test(token)) {
+      hasWeaklyLtr = true;
+    }
+  }
+
+  return totalCount == 0 ?
+      (hasWeaklyLtr ? goog.i18n.bidi.Dir.LTR : goog.i18n.bidi.Dir.NEUTRAL) :
+      (rtlCount / totalCount > goog.i18n.bidi.rtlDetectionThreshold_ ?
+           goog.i18n.bidi.Dir.RTL :
+           goog.i18n.bidi.Dir.LTR);
+};
+
+
+/**
+ * Check the directionality of a piece of text, return true if the piece of
+ * text should be laid out in RTL direction.
+ * @param {string} str The piece of text that need to be detected.
+ * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {boolean} Whether this piece of text should be laid out in RTL.
+ */
+goog.i18n.bidi.detectRtlDirectionality = function(str, opt_isHtml) {
+  return goog.i18n.bidi.estimateDirection(str, opt_isHtml) ==
+      goog.i18n.bidi.Dir.RTL;
+};
+
+
+/**
+ * Sets text input element's directionality and text alignment based on a
+ * given directionality. Does nothing if the given directionality is unknown or
+ * neutral.
+ * @param {Element} element Input field element to set directionality to.
+ * @param {goog.i18n.bidi.Dir|number|boolean|null} dir Desired directionality,
+ *     given in one of the following formats:
+ *     1. A goog.i18n.bidi.Dir constant.
+ *     2. A number (positive = LRT, negative = RTL, 0 = neutral).
+ *     3. A boolean (true = RTL, false = LTR).
+ *     4. A null for unknown directionality.
+ */
+goog.i18n.bidi.setElementDirAndAlign = function(element, dir) {
+  if (element) {
+    dir = goog.i18n.bidi.toDir(dir);
+    if (dir) {
+      element.style.textAlign = dir == goog.i18n.bidi.Dir.RTL ?
+          goog.i18n.bidi.RIGHT :
+          goog.i18n.bidi.LEFT;
+      element.dir = dir == goog.i18n.bidi.Dir.RTL ? 'rtl' : 'ltr';
+    }
+  }
+};
+
+
+/**
+ * Sets element dir based on estimated directionality of the given text.
+ * @param {!Element} element
+ * @param {string} text
+ */
+goog.i18n.bidi.setElementDirByTextDirectionality = function(element, text) {
+  switch (goog.i18n.bidi.estimateDirection(text)) {
+    case (goog.i18n.bidi.Dir.LTR):
+      element.dir = 'ltr';
+      break;
+    case (goog.i18n.bidi.Dir.RTL):
+      element.dir = 'rtl';
+      break;
+    default:
+      // Default for no direction, inherit from document.
+      element.removeAttribute('dir');
+  }
+};
+
+
+
+/**
+ * Strings that have an (optional) known direction.
+ *
+ * Implementations of this interface are string-like objects that carry an
+ * attached direction, if known.
+ * @interface
+ */
+goog.i18n.bidi.DirectionalString = function() {};
+
+
+/**
+ * Interface marker of the DirectionalString interface.
+ *
+ * This property can be used to determine at runtime whether or not an object
+ * implements this interface.  All implementations of this interface set this
+ * property to {@code true}.
+ * @type {boolean}
+ */
+goog.i18n.bidi.DirectionalString.prototype
+    .implementsGoogI18nBidiDirectionalString;
+
+
+/**
+ * Retrieves this object's known direction (if any).
+ * @return {?goog.i18n.bidi.Dir} The known direction. Null if unknown.
+ */
+goog.i18n.bidi.DirectionalString.prototype.getDirection;
diff --git a/third_party/ink/closure/i18n/bidiformatter.js b/third_party/ink/closure/i18n/bidiformatter.js
new file mode 100644
index 0000000..19bb2dd
--- /dev/null
+++ b/third_party/ink/closure/i18n/bidiformatter.js
@@ -0,0 +1,556 @@
+// Copyright 2009 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 Utility for formatting text for display in a potentially
+ * opposite-directionality context without garbling.
+ * Mostly a port of http://go/formatter.cc.
+ * @author tomerigo@google.com (Tomer Greenberg)
+ */
+
+
+goog.provide('goog.i18n.BidiFormatter');
+
+goog.require('goog.html.SafeHtml');
+goog.require('goog.i18n.bidi');
+goog.require('goog.i18n.bidi.Dir');
+goog.require('goog.i18n.bidi.Format');
+
+
+
+/**
+ * Utility class for formatting text for display in a potentially
+ * opposite-directionality context without garbling. Provides the following
+ * functionality:
+ *
+ * 1. BiDi Wrapping
+ * When text in one language is mixed into a document in another, opposite-
+ * directionality language, e.g. when an English business name is embedded in a
+ * Hebrew web page, both the inserted string and the text following it may be
+ * displayed incorrectly unless the inserted string is explicitly separated
+ * from the surrounding text in a "wrapper" that declares its directionality at
+ * the start and then resets it back at the end. This wrapping can be done in
+ * HTML mark-up (e.g. a 'span dir="rtl"' tag) or - only in contexts where
+ * mark-up can not be used - in Unicode BiDi formatting codes (LRE|RLE and PDF).
+ * Providing such wrapping services is the basic purpose of the BiDi formatter.
+ *
+ * 2. Directionality estimation
+ * How does one know whether a string about to be inserted into surrounding
+ * text has the same directionality? Well, in many cases, one knows that this
+ * must be the case when writing the code doing the insertion, e.g. when a
+ * localized message is inserted into a localized page. In such cases there is
+ * no need to involve the BiDi formatter at all. In the remaining cases, e.g.
+ * when the string is user-entered or comes from a database, the language of
+ * the string (and thus its directionality) is not known a priori, and must be
+ * estimated at run-time. The BiDi formatter does this automatically.
+ *
+ * 3. Escaping
+ * When wrapping plain text - i.e. text that is not already HTML or HTML-
+ * escaped - in HTML mark-up, the text must first be HTML-escaped to prevent XSS
+ * attacks and other nasty business. This of course is always true, but the
+ * escaping can not be done after the string has already been wrapped in
+ * mark-up, so the BiDi formatter also serves as a last chance and includes
+ * escaping services.
+ *
+ * Thus, in a single call, the formatter will escape the input string as
+ * specified, determine its directionality, and wrap it as necessary. It is
+ * then up to the caller to insert the return value in the output.
+ *
+ * See http://wiki/Main/TemplatesAndBiDi for more information.
+ *
+ * @param {goog.i18n.bidi.Dir|number|boolean|null} contextDir The context
+ *     directionality, in one of the following formats:
+ *     1. A goog.i18n.bidi.Dir constant. NEUTRAL is treated the same as null,
+ *        i.e. unknown, for backward compatibility with legacy calls.
+ *     2. A number (positive = LTR, negative = RTL, 0 = unknown).
+ *     3. A boolean (true = RTL, false = LTR).
+ *     4. A null for unknown directionality.
+ * @param {boolean=} opt_alwaysSpan Whether {@link #spanWrap} should always
+ *     use a 'span' tag, even when the input directionality is neutral or
+ *     matches the context, so that the DOM structure of the output does not
+ *     depend on the combination of directionalities. Default: false.
+ * @constructor
+ * @final
+ */
+goog.i18n.BidiFormatter = function(contextDir, opt_alwaysSpan) {
+  /**
+   * The overall directionality of the context in which the formatter is being
+   * used.
+   * @type {?goog.i18n.bidi.Dir}
+   * @private
+   */
+  this.contextDir_ = goog.i18n.bidi.toDir(contextDir, true /* opt_noNeutral */);
+
+  /**
+   * Whether {@link #spanWrap} and similar methods should always use the same
+   * span structure, regardless of the combination of directionalities, for a
+   * stable DOM structure.
+   * @type {boolean}
+   * @private
+   */
+  this.alwaysSpan_ = !!opt_alwaysSpan;
+};
+
+
+/**
+ * @return {?goog.i18n.bidi.Dir} The context directionality.
+ */
+goog.i18n.BidiFormatter.prototype.getContextDir = function() {
+  return this.contextDir_;
+};
+
+
+/**
+ * @return {boolean} Whether alwaysSpan is set.
+ */
+goog.i18n.BidiFormatter.prototype.getAlwaysSpan = function() {
+  return this.alwaysSpan_;
+};
+
+
+/**
+ * @param {goog.i18n.bidi.Dir|number|boolean|null} contextDir The context
+ *     directionality, in one of the following formats:
+ *     1. A goog.i18n.bidi.Dir constant. NEUTRAL is treated the same as null,
+ *        i.e. unknown.
+ *     2. A number (positive = LTR, negative = RTL, 0 = unknown).
+ *     3. A boolean (true = RTL, false = LTR).
+ *     4. A null for unknown directionality.
+ */
+goog.i18n.BidiFormatter.prototype.setContextDir = function(contextDir) {
+  this.contextDir_ = goog.i18n.bidi.toDir(contextDir, true /* opt_noNeutral */);
+};
+
+
+/**
+ * @param {boolean} alwaysSpan Whether {@link #spanWrap} should always use a
+ *     'span' tag, even when the input directionality is neutral or matches the
+ *     context, so that the DOM structure of the output does not depend on the
+ *     combination of directionalities.
+ */
+goog.i18n.BidiFormatter.prototype.setAlwaysSpan = function(alwaysSpan) {
+  this.alwaysSpan_ = alwaysSpan;
+};
+
+
+/**
+ * Returns the directionality of input argument {@code str}.
+ * Identical to {@link goog.i18n.bidi.estimateDirection}.
+ *
+ * @param {string} str The input text.
+ * @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {goog.i18n.bidi.Dir} Estimated overall directionality of {@code str}.
+ */
+goog.i18n.BidiFormatter.prototype.estimateDirection =
+    goog.i18n.bidi.estimateDirection;
+
+
+/**
+ * Returns true if two given directionalities are opposite.
+ * Note: the implementation is based on the numeric values of the Dir enum.
+ *
+ * @param {?goog.i18n.bidi.Dir} dir1 1st directionality.
+ * @param {?goog.i18n.bidi.Dir} dir2 2nd directionality.
+ * @return {boolean} Whether the directionalities are opposite.
+ * @private
+ */
+goog.i18n.BidiFormatter.prototype.areDirectionalitiesOpposite_ = function(
+    dir1, dir2) {
+  return Number(dir1) * Number(dir2) < 0;
+};
+
+
+/**
+ * Returns a unicode BiDi mark matching the context directionality (LRM or
+ * RLM) if {@code opt_dirReset}, and if either the directionality or the exit
+ * directionality of {@code str} is opposite to the context directionality.
+ * Otherwise returns the empty string.
+ *
+ * @param {string} str The input text.
+ * @param {goog.i18n.bidi.Dir} dir {@code str}'s overall directionality.
+ * @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.
+ *     Default: false.
+ * @param {boolean=} opt_dirReset Whether to perform the reset. Default: false.
+ * @return {string} A unicode BiDi mark or the empty string.
+ * @private
+ */
+goog.i18n.BidiFormatter.prototype.dirResetIfNeeded_ = function(
+    str, dir, opt_isHtml, opt_dirReset) {
+  // endsWithRtl and endsWithLtr are called only if needed (short-circuit).
+  if (opt_dirReset &&
+      (this.areDirectionalitiesOpposite_(dir, this.contextDir_) ||
+       (this.contextDir_ == goog.i18n.bidi.Dir.LTR &&
+        goog.i18n.bidi.endsWithRtl(str, opt_isHtml)) ||
+       (this.contextDir_ == goog.i18n.bidi.Dir.RTL &&
+        goog.i18n.bidi.endsWithLtr(str, opt_isHtml)))) {
+    return this.contextDir_ == goog.i18n.bidi.Dir.LTR ?
+        goog.i18n.bidi.Format.LRM :
+        goog.i18n.bidi.Format.RLM;
+  } else {
+    return '';
+  }
+};
+
+
+/**
+ * Returns "rtl" if {@code str}'s estimated directionality is RTL, and "ltr" if
+ * it is LTR. In case it's NEUTRAL, returns "rtl" if the context directionality
+ * is RTL, and "ltr" otherwise.
+ * Needed for GXP, which can't handle dirAttr.
+ * Example use case:
+ * &lt;td expr:dir='bidiFormatter.dirAttrValue(foo)'&gt;
+ *   &lt;gxp:eval expr='foo'&gt;
+ * &lt;/td&gt;
+ *
+ * @param {string} str Text whose directionality is to be estimated.
+ * @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {string} "rtl" or "ltr", according to the logic described above.
+ */
+goog.i18n.BidiFormatter.prototype.dirAttrValue = function(str, opt_isHtml) {
+  return this.knownDirAttrValue(this.estimateDirection(str, opt_isHtml));
+};
+
+
+/**
+ * Returns "rtl" if the given directionality is RTL, and "ltr" if it is LTR. In
+ * case it's NEUTRAL, returns "rtl" if the context directionality is RTL, and
+ * "ltr" otherwise.
+ *
+ * @param {goog.i18n.bidi.Dir} dir A directionality.
+ * @return {string} "rtl" or "ltr", according to the logic described above.
+ */
+goog.i18n.BidiFormatter.prototype.knownDirAttrValue = function(dir) {
+  var resolvedDir = dir == goog.i18n.bidi.Dir.NEUTRAL ? this.contextDir_ : dir;
+  return resolvedDir == goog.i18n.bidi.Dir.RTL ? 'rtl' : 'ltr';
+};
+
+
+/**
+ * Returns 'dir="ltr"' or 'dir="rtl"', depending on {@code str}'s estimated
+ * directionality, if it is not the same as the context directionality.
+ * Otherwise, returns the empty string.
+ *
+ * @param {string} str Text whose directionality is to be estimated.
+ * @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {string} 'dir="rtl"' for RTL text in non-RTL context; 'dir="ltr"' for
+ *     LTR text in non-LTR context; else, the empty string.
+ */
+goog.i18n.BidiFormatter.prototype.dirAttr = function(str, opt_isHtml) {
+  return this.knownDirAttr(this.estimateDirection(str, opt_isHtml));
+};
+
+
+/**
+ * Returns 'dir="ltr"' or 'dir="rtl"', depending on the given directionality, if
+ * it is not the same as the context directionality. Otherwise, returns the
+ * empty string.
+ *
+ * @param {goog.i18n.bidi.Dir} dir A directionality.
+ * @return {string} 'dir="rtl"' for RTL text in non-RTL context; 'dir="ltr"' for
+ *     LTR text in non-LTR context; else, the empty string.
+ */
+goog.i18n.BidiFormatter.prototype.knownDirAttr = function(dir) {
+  if (dir != this.contextDir_) {
+    return dir == goog.i18n.bidi.Dir.RTL ?
+        'dir="rtl"' :
+        dir == goog.i18n.bidi.Dir.LTR ? 'dir="ltr"' : '';
+  }
+  return '';
+};
+
+
+/**
+ * Formats a string of unknown directionality for use in HTML output of the
+ * context directionality, so an opposite-directionality string is neither
+ * garbled nor garbles what follows it.
+ * The algorithm: estimates the directionality of input argument {@code html}.
+ * In case its directionality doesn't match the context directionality, wraps it
+ * with a 'span' tag and adds a "dir" attribute (either 'dir="rtl"' or
+ * 'dir="ltr"'). If setAlwaysSpan(true) was used, the input is always wrapped
+ * with 'span', skipping just the dir attribute when it's not needed.
+ *
+ * If {@code opt_dirReset}, and if the overall directionality or the exit
+ * directionality of {@code str} are opposite to the context directionality, a
+ * trailing unicode BiDi mark matching the context directionality is appened
+ * (LRM or RLM).
+ *
+ * @param {!goog.html.SafeHtml} html The input HTML.
+ * @param {boolean=} opt_dirReset Whether to append a trailing unicode bidi mark
+ *     matching the context directionality, when needed, to prevent the possible
+ *     garbling of whatever may follow {@code html}. Default: true.
+ * @return {!goog.html.SafeHtml} Input text after applying the processing.
+ */
+goog.i18n.BidiFormatter.prototype.spanWrapSafeHtml = function(
+    html, opt_dirReset) {
+  return this.spanWrapSafeHtmlWithKnownDir(null, html, opt_dirReset);
+};
+
+
+/**
+ * Formats a string of given directionality for use in HTML output of the
+ * context directionality, so an opposite-directionality string is neither
+ * garbled nor garbles what follows it.
+ * The algorithm: If {@code dir} doesn't match the context directionality, wraps
+ * {@code html} with a 'span' tag and adds a "dir" attribute (either 'dir="rtl"'
+ * or 'dir="ltr"'). If setAlwaysSpan(true) was used, the input is always wrapped
+ * with 'span', skipping just the dir attribute when it's not needed.
+ *
+ * If {@code opt_dirReset}, and if {@code dir} or the exit directionality of
+ * {@code html} are opposite to the context directionality, a trailing unicode
+ * BiDi mark matching the context directionality is appened (LRM or RLM).
+ *
+ * @param {?goog.i18n.bidi.Dir} dir {@code html}'s overall directionality, or
+ *     null if unknown and needs to be estimated.
+ * @param {!goog.html.SafeHtml} html The input HTML.
+ * @param {boolean=} opt_dirReset Whether to append a trailing unicode bidi mark
+ *     matching the context directionality, when needed, to prevent the possible
+ *     garbling of whatever may follow {@code html}. Default: true.
+ * @return {!goog.html.SafeHtml} Input text after applying the processing.
+ */
+goog.i18n.BidiFormatter.prototype.spanWrapSafeHtmlWithKnownDir = function(
+    dir, html, opt_dirReset) {
+  if (dir == null) {
+    dir = this.estimateDirection(goog.html.SafeHtml.unwrap(html), true);
+  }
+  return this.spanWrapWithKnownDir_(dir, html, opt_dirReset);
+};
+
+
+/**
+ * The internal implementation of spanWrapSafeHtmlWithKnownDir for non-null dir,
+ * to help the compiler optimize.
+ *
+ * @param {goog.i18n.bidi.Dir} dir {@code str}'s overall directionality.
+ * @param {!goog.html.SafeHtml} html The input HTML.
+ * @param {boolean=} opt_dirReset Whether to append a trailing unicode bidi mark
+ *     matching the context directionality, when needed, to prevent the possible
+ *     garbling of whatever may follow {@code str}. Default: true.
+ * @return {!goog.html.SafeHtml} Input text after applying the above processing.
+ * @private
+ */
+goog.i18n.BidiFormatter.prototype.spanWrapWithKnownDir_ = function(
+    dir, html, opt_dirReset) {
+  opt_dirReset = opt_dirReset || (opt_dirReset == undefined);
+
+  var result;
+  // Whether to add the "dir" attribute.
+  var dirCondition =
+      dir != goog.i18n.bidi.Dir.NEUTRAL && dir != this.contextDir_;
+  if (this.alwaysSpan_ || dirCondition) {  // Wrap is needed
+    var dirAttribute;
+    if (dirCondition) {
+      dirAttribute = dir == goog.i18n.bidi.Dir.RTL ? 'rtl' : 'ltr';
+    }
+    result = goog.html.SafeHtml.create('span', {'dir': dirAttribute}, html);
+  } else {
+    result = html;
+  }
+  var str = goog.html.SafeHtml.unwrap(html);
+  result = goog.html.SafeHtml.concatWithDir(
+      goog.i18n.bidi.Dir.NEUTRAL, result,
+      this.dirResetIfNeeded_(str, dir, true, opt_dirReset));
+  return result;
+};
+
+
+/**
+ * Formats a string of unknown directionality for use in plain-text output of
+ * the context directionality, so an opposite-directionality string is neither
+ * garbled nor garbles what follows it.
+ * As opposed to {@link #spanWrap}, this makes use of unicode BiDi formatting
+ * characters. In HTML, its *only* valid use is inside of elements that do not
+ * allow mark-up, e.g. an 'option' tag.
+ * The algorithm: estimates the directionality of input argument {@code str}.
+ * In case it doesn't match  the context directionality, wraps it with Unicode
+ * BiDi formatting characters: RLE{@code str}PDF for RTL text, and
+ * LRE{@code str}PDF for LTR text.
+ *
+ * If {@code opt_dirReset}, and if the overall directionality or the exit
+ * directionality of {@code str} are opposite to the context directionality, a
+ * trailing unicode BiDi mark matching the context directionality is appended
+ * (LRM or RLM).
+ *
+ * Does *not* do HTML-escaping regardless of the value of {@code opt_isHtml}.
+ * The return value can be HTML-escaped as necessary.
+ *
+ * @param {string} str The input text.
+ * @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.
+ *     Default: false.
+ * @param {boolean=} opt_dirReset Whether to append a trailing unicode bidi mark
+ *     matching the context directionality, when needed, to prevent the possible
+ *     garbling of whatever may follow {@code str}. Default: true.
+ * @return {string} Input text after applying the above processing.
+ */
+goog.i18n.BidiFormatter.prototype.unicodeWrap = function(
+    str, opt_isHtml, opt_dirReset) {
+  return this.unicodeWrapWithKnownDir(null, str, opt_isHtml, opt_dirReset);
+};
+
+
+/**
+ * Formats a string of given directionality for use in plain-text output of the
+ * context directionality, so an opposite-directionality string is neither
+ * garbled nor garbles what follows it.
+ * As opposed to {@link #spanWrapWithKnownDir}, makes use of unicode BiDi
+ * formatting characters. In HTML, its *only* valid use is inside of elements
+ * that do not allow mark-up, e.g. an 'option' tag.
+ * The algorithm: If {@code dir} doesn't match the context directionality, wraps
+ * {@code str} with Unicode BiDi formatting characters: RLE{@code str}PDF for
+ * RTL text, and LRE{@code str}PDF for LTR text.
+ *
+ * If {@code opt_dirReset}, and if the overall directionality or the exit
+ * directionality of {@code str} are opposite to the context directionality, a
+ * trailing unicode BiDi mark matching the context directionality is appended
+ * (LRM or RLM).
+ *
+ * Does *not* do HTML-escaping regardless of the value of {@code opt_isHtml}.
+ * The return value can be HTML-escaped as necessary.
+ *
+ * @param {?goog.i18n.bidi.Dir} dir {@code str}'s overall directionality, or
+ *     null if unknown and needs to be estimated.
+ * @param {string} str The input text.
+ * @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.
+ *     Default: false.
+ * @param {boolean=} opt_dirReset Whether to append a trailing unicode bidi mark
+ *     matching the context directionality, when needed, to prevent the possible
+ *     garbling of whatever may follow {@code str}. Default: true.
+ * @return {string} Input text after applying the above processing.
+ */
+goog.i18n.BidiFormatter.prototype.unicodeWrapWithKnownDir = function(
+    dir, str, opt_isHtml, opt_dirReset) {
+  if (dir == null) {
+    dir = this.estimateDirection(str, opt_isHtml);
+  }
+  return this.unicodeWrapWithKnownDir_(dir, str, opt_isHtml, opt_dirReset);
+};
+
+
+/**
+ * The internal implementation of unicodeWrapWithKnownDir for non-null dir, to
+ * help the compiler optimize.
+ *
+ * @param {goog.i18n.bidi.Dir} dir {@code str}'s overall directionality.
+ * @param {string} str The input text.
+ * @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.
+ *     Default: false.
+ * @param {boolean=} opt_dirReset Whether to append a trailing unicode bidi mark
+ *     matching the context directionality, when needed, to prevent the possible
+ *     garbling of whatever may follow {@code str}. Default: true.
+ * @return {string} Input text after applying the above processing.
+ * @private
+ */
+goog.i18n.BidiFormatter.prototype.unicodeWrapWithKnownDir_ = function(
+    dir, str, opt_isHtml, opt_dirReset) {
+  opt_dirReset = opt_dirReset || (opt_dirReset == undefined);
+  var result = [];
+  if (dir != goog.i18n.bidi.Dir.NEUTRAL && dir != this.contextDir_) {
+    result.push(
+        dir == goog.i18n.bidi.Dir.RTL ? goog.i18n.bidi.Format.RLE :
+                                        goog.i18n.bidi.Format.LRE);
+    result.push(str);
+    result.push(goog.i18n.bidi.Format.PDF);
+  } else {
+    result.push(str);
+  }
+
+  result.push(this.dirResetIfNeeded_(str, dir, opt_isHtml, opt_dirReset));
+  return result.join('');
+};
+
+
+/**
+ * Returns a Unicode BiDi mark matching the context directionality (LRM or RLM)
+ * if the directionality or the exit directionality of {@code str} are opposite
+ * to the context directionality. Otherwise returns the empty string.
+ *
+ * @param {string} str The input text.
+ * @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {string} A Unicode bidi mark matching the global directionality or
+ *     the empty string.
+ */
+goog.i18n.BidiFormatter.prototype.markAfter = function(str, opt_isHtml) {
+  return this.markAfterKnownDir(null, str, opt_isHtml);
+};
+
+
+/**
+ * Returns a Unicode BiDi mark matching the context directionality (LRM or RLM)
+ * if the given directionality or the exit directionality of {@code str} are
+ * opposite to the context directionality. Otherwise returns the empty string.
+ *
+ * @param {?goog.i18n.bidi.Dir} dir {@code str}'s overall directionality, or
+ *     null if unknown and needs to be estimated.
+ * @param {string} str The input text.
+ * @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.
+ *     Default: false.
+ * @return {string} A Unicode bidi mark matching the global directionality or
+ *     the empty string.
+ */
+goog.i18n.BidiFormatter.prototype.markAfterKnownDir = function(
+    dir, str, opt_isHtml) {
+  if (dir == null) {
+    dir = this.estimateDirection(str, opt_isHtml);
+  }
+  return this.dirResetIfNeeded_(str, dir, opt_isHtml, true);
+};
+
+
+/**
+ * Returns the Unicode BiDi mark matching the context directionality (LRM for
+ * LTR context directionality, RLM for RTL context directionality), or the
+ * empty string for neutral / unknown context directionality.
+ *
+ * @return {string} LRM for LTR context directionality and RLM for RTL context
+ *     directionality.
+ */
+goog.i18n.BidiFormatter.prototype.mark = function() {
+  switch (this.contextDir_) {
+    case (goog.i18n.bidi.Dir.LTR):
+      return goog.i18n.bidi.Format.LRM;
+    case (goog.i18n.bidi.Dir.RTL):
+      return goog.i18n.bidi.Format.RLM;
+    default:
+      return '';
+  }
+};
+
+
+/**
+ * Returns 'right' for RTL context directionality. Otherwise (LTR or neutral /
+ * unknown context directionality) returns 'left'.
+ *
+ * @return {string} 'right' for RTL context directionality and 'left' for other
+ *     context directionality.
+ */
+goog.i18n.BidiFormatter.prototype.startEdge = function() {
+  return this.contextDir_ == goog.i18n.bidi.Dir.RTL ? goog.i18n.bidi.RIGHT :
+                                                      goog.i18n.bidi.LEFT;
+};
+
+
+/**
+ * Returns 'left' for RTL context directionality. Otherwise (LTR or neutral /
+ * unknown context directionality) returns 'right'.
+ *
+ * @return {string} 'left' for RTL context directionality and 'right' for other
+ *     context directionality.
+ */
+goog.i18n.BidiFormatter.prototype.endEdge = function() {
+  return this.contextDir_ == goog.i18n.bidi.Dir.RTL ? goog.i18n.bidi.LEFT :
+                                                      goog.i18n.bidi.RIGHT;
+};
diff --git a/third_party/ink/closure/i18n/graphemebreak.js b/third_party/ink/closure/i18n/graphemebreak.js
new file mode 100644
index 0000000..ba85f94
--- /dev/null
+++ b/third_party/ink/closure/i18n/graphemebreak.js
@@ -0,0 +1,451 @@
+// Copyright 2006 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 Detect Grapheme Cluster Break in a pair of codepoints. Follows
+ * Unicode 10 UAX#29. Tailoring for Virama × Indic Letters is used.
+ *
+ * Reference: http://unicode.org/reports/tr29
+ *
+ * @author cibu@google.com (Cibu Johny)
+ * @author fabalbon@google.com (Felipe Balbontin)
+ */
+
+goog.provide('goog.i18n.GraphemeBreak');
+
+goog.require('goog.asserts');
+goog.require('goog.i18n.uChar');
+goog.require('goog.structs.InversionMap');
+
+/**
+ * Enum for all Grapheme Cluster Break properties.
+ * These enums directly corresponds to Grapheme_Cluster_Break property values
+ * mentioned in http://unicode.org/reports/tr29 table 2. VIRAMA and
+ * INDIC_LETTER are for the Virama × Base tailoring mentioned in the notes.
+ *
+ * @protected @enum {number}
+ */
+goog.i18n.GraphemeBreak.property = {
+  OTHER: 0,
+  CONTROL: 1,
+  EXTEND: 2,
+  PREPEND: 3,
+  SPACING_MARK: 4,
+  INDIC_LETTER: 5,
+  VIRAMA: 6,
+  L: 7,
+  V: 8,
+  T: 9,
+  LV: 10,
+  LVT: 11,
+  CR: 12,
+  LF: 13,
+  REGIONAL_INDICATOR: 14,
+  ZWJ: 15,
+  E_BASE: 16,
+  GLUE_AFTER_ZWJ: 17,
+  E_MODIFIER: 18,
+  E_BASE_GAZ: 19
+};
+
+
+/**
+ * Grapheme Cluster Break property values for all codepoints as inversion map.
+ * Constructed lazily.
+ *
+ * @private {?goog.structs.InversionMap}
+ */
+goog.i18n.GraphemeBreak.inversions_ = null;
+
+
+/**
+ * Indicates if a and b form a grapheme cluster.
+ *
+ * This implements the rules in:
+ * http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules
+ *
+ * @param {number|string} a Code point or string with the first side of
+ *     grapheme cluster.
+ * @param {number|string} b Code point or string with the second side of
+ *     grapheme cluster.
+ * @param {boolean} extended If true, indicates extended grapheme cluster;
+ *     If false, indicates legacy cluster.
+ * @return {boolean} True if a & b do not form a cluster; False otherwise.
+ * @private
+ */
+goog.i18n.GraphemeBreak.applyBreakRules_ = function(a, b, extended) {
+  var prop = goog.i18n.GraphemeBreak.property;
+
+  var aCode = goog.isString(a) ?
+      goog.i18n.GraphemeBreak.getCodePoint_(a, a.length - 1) :
+      a;
+  var bCode =
+      goog.isString(b) ? goog.i18n.GraphemeBreak.getCodePoint_(b, 0) : b;
+
+  var aProp = goog.i18n.GraphemeBreak.getBreakProp_(aCode);
+  var bProp = goog.i18n.GraphemeBreak.getBreakProp_(bCode);
+
+  var isString = goog.isString(a);
+
+  // GB3.
+  if (aProp === prop.CR && bProp === prop.LF) {
+    return false;
+  }
+
+  // GB4.
+  if (aProp === prop.CONTROL || aProp === prop.CR || aProp === prop.LF) {
+    return true;
+  }
+
+  // GB5.
+  if (bProp === prop.CONTROL || bProp === prop.CR || bProp === prop.LF) {
+    return true;
+  }
+
+  // GB6.
+  if (aProp === prop.L &&
+      (bProp === prop.L || bProp === prop.V || bProp === prop.LV ||
+       bProp === prop.LVT)) {
+    return false;
+  }
+
+  // GB7.
+  if ((aProp === prop.LV || aProp === prop.V) &&
+      (bProp === prop.V || bProp === prop.T)) {
+    return false;
+  }
+
+  // GB8.
+  if ((aProp === prop.LVT || aProp === prop.T) && bProp === prop.T) {
+    return false;
+  }
+
+  // GB9.
+  if (bProp === prop.EXTEND || bProp === prop.ZWJ || bProp === prop.VIRAMA) {
+    return false;
+  }
+
+  // GB9a, GB9b.
+  if (extended && (aProp === prop.PREPEND || bProp === prop.SPACING_MARK)) {
+    return false;
+  }
+
+  // Tailorings for basic aksara support.
+  if (extended && aProp === prop.VIRAMA && bProp === prop.INDIC_LETTER) {
+    return false;
+  }
+
+  var aStr, index, codePoint, codePointProp;
+
+  // GB10.
+  if (isString) {
+    if (bProp === prop.E_MODIFIER) {
+      // If using new API, consume the string's code points starting from the
+      // end and test the left side of: (E_Base | EBG) Extend* × E_Modifier.
+      aStr = /** @type {string} */ (a);
+      index = aStr.length - 1;
+      codePoint = aCode;
+      codePointProp = aProp;
+      while (index > 0 && codePointProp === prop.EXTEND) {
+        index -= goog.i18n.uChar.charCount(codePoint);
+        codePoint = goog.i18n.GraphemeBreak.getCodePoint_(aStr, index);
+        codePointProp = goog.i18n.GraphemeBreak.getBreakProp_(codePoint);
+      }
+      if (codePointProp === prop.E_BASE || codePointProp === prop.E_BASE_GAZ) {
+        return false;
+      }
+    }
+  } else {
+    // If using legacy API, return best effort by testing:
+    // (E_Base | EBG) × E_Modifier.
+    if ((aProp === prop.E_BASE || aProp === prop.E_BASE_GAZ) &&
+        bProp === prop.E_MODIFIER) {
+      return false;
+    }
+  }
+
+  // GB11.
+  if (aProp === prop.ZWJ &&
+      (bProp === prop.GLUE_AFTER_ZWJ || bProp === prop.E_BASE_GAZ)) {
+    return false;
+  }
+
+  // GB12, GB13.
+  if (isString) {
+    if (bProp === prop.REGIONAL_INDICATOR) {
+      // If using new API, consume the string's code points starting from the
+      // end and test the left side of these rules:
+      // - sot (RI RI)* RI × RI
+      // - [^RI] (RI RI)* RI × RI.
+      var numberOfRi = 0;
+      aStr = /** @type {string} */ (a);
+      index = aStr.length - 1;
+      codePoint = aCode;
+      codePointProp = aProp;
+      while (index > 0 && codePointProp === prop.REGIONAL_INDICATOR) {
+        numberOfRi++;
+        index -= goog.i18n.uChar.charCount(codePoint);
+        codePoint = goog.i18n.GraphemeBreak.getCodePoint_(aStr, index);
+        codePointProp = goog.i18n.GraphemeBreak.getBreakProp_(codePoint);
+      }
+      if (codePointProp === prop.REGIONAL_INDICATOR) {
+        numberOfRi++;
+      }
+      if (numberOfRi % 2 === 1) {
+        return false;
+      }
+    }
+  } else {
+    // If using legacy API, return best effort by testing: RI × RI.
+    if (aProp === prop.REGIONAL_INDICATOR &&
+        bProp === prop.REGIONAL_INDICATOR) {
+      return false;
+    }
+  }
+
+  // GB999.
+  return true;
+};
+
+
+/**
+ * Method to return property enum value of the code point. If it is Hangul LV or
+ * LVT, then it is computed; for the rest it is picked from the inversion map.
+ *
+ * @param {number} codePoint The code point value of the character.
+ * @return {number} Property enum value of code point.
+ * @private
+ */
+goog.i18n.GraphemeBreak.getBreakProp_ = function(codePoint) {
+  if (0xAC00 <= codePoint && codePoint <= 0xD7A3) {
+    var prop = goog.i18n.GraphemeBreak.property;
+    if (codePoint % 0x1C === 0x10) {
+      return prop.LV;
+    }
+    return prop.LVT;
+  } else {
+    if (!goog.i18n.GraphemeBreak.inversions_) {
+      goog.i18n.GraphemeBreak.inversions_ = new goog.structs.InversionMap(
+          [
+            0,      10,   1,     2,   1,    18,   95,    33,    13,  1,
+            594,    112,  275,   7,   263,  45,   1,     1,     1,   2,
+            1,      2,    1,     1,   56,   6,    10,    11,    1,   1,
+            46,     21,   16,    1,   101,  7,    1,     1,     6,   2,
+            2,      1,    4,     33,  1,    1,    1,     30,    27,  91,
+            11,     58,   9,     34,  4,    1,    9,     1,     3,   1,
+            5,      43,   3,     120, 14,   1,    32,    1,     17,  37,
+            1,      1,    1,     1,   3,    8,    4,     1,     2,   1,
+            7,      8,    2,     2,   21,   7,    1,     1,     2,   17,
+            39,     1,    1,     1,   2,    6,    6,     1,     9,   5,
+            4,      2,    2,     12,  2,    15,   2,     1,     17,  39,
+            2,      3,    12,    4,   8,    6,    17,    2,     3,   14,
+            1,      17,   39,    1,   1,    3,    8,     4,     1,   20,
+            2,      29,   1,     2,   17,   39,   1,     1,     2,   1,
+            6,      6,    9,     6,   4,    2,    2,     13,    1,   16,
+            1,      18,   41,    1,   1,    1,    12,    1,     9,   1,
+            40,     1,    3,     17,  31,   1,    5,     4,     3,   5,
+            7,      8,    3,     2,   8,    2,    29,    1,     2,   17,
+            39,     1,    1,     1,   1,    2,    1,     3,     1,   5,
+            1,      8,    9,     1,   3,    2,    29,    1,     2,   17,
+            38,     3,    1,     2,   5,    7,    1,     1,     8,   1,
+            10,     2,    30,    2,   22,   48,   5,     1,     2,   6,
+            7,      1,    18,    2,   13,   46,   2,     1,     1,   1,
+            6,      1,    12,    8,   50,   46,   2,     1,     1,   1,
+            9,      11,   6,     14,  2,    58,   2,     27,    1,   1,
+            1,      1,    1,     4,   2,    49,   14,    1,     4,   1,
+            1,      2,    5,     48,  9,    1,    57,    33,    12,  4,
+            1,      6,    1,     2,   2,    2,    1,     16,    2,   4,
+            2,      2,    4,     3,   1,    3,    2,     7,     3,   4,
+            13,     1,    1,     1,   2,    6,    1,     1,     14,  1,
+            98,     96,   72,    88,  349,  3,    931,   15,    2,   1,
+            14,     15,   2,     1,   14,   15,   2,     15,    15,  14,
+            35,     17,   2,     1,   7,    8,    1,     2,     9,   1,
+            1,      9,    1,     45,  3,    1,    118,   2,     34,  1,
+            87,     28,   3,     3,   4,    2,    9,     1,     6,   3,
+            20,     19,   29,    44,  84,   23,   2,     2,     1,   4,
+            45,     6,    2,     1,   1,    1,    8,     1,     1,   1,
+            2,      8,    6,     13,  48,   84,   1,     14,    33,  1,
+            1,      5,    1,     1,   5,    1,    1,     1,     7,   31,
+            9,      12,   2,     1,   7,    23,   1,     4,     2,   2,
+            2,      2,    2,     11,  3,    2,    36,    2,     1,   1,
+            2,      3,    1,     1,   3,    2,    12,    36,    8,   8,
+            2,      2,    21,    3,   128,  3,    1,     13,    1,   7,
+            4,      1,    4,     2,   1,    3,    2,     198,   64,  523,
+            1,      1,    1,     2,   24,   7,    49,    16,    96,  33,
+            1324,   1,    34,    1,   1,    1,    82,    2,     98,  1,
+            14,     1,    1,     4,   86,   1,    1418,  3,     141, 1,
+            96,     32,   554,   6,   105,  2,    30164, 4,     1,   10,
+            32,     2,    80,    2,   272,  1,    3,     1,     4,   1,
+            23,     2,    2,     1,   24,   30,   4,     4,     3,   8,
+            1,      1,    13,    2,   16,   34,   16,    1,     1,   26,
+            18,     24,   24,    4,   8,    2,    23,    11,    1,   1,
+            12,     32,   3,     1,   5,    3,    3,     36,    1,   2,
+            4,      2,    1,     3,   1,    36,   1,     32,    35,  6,
+            2,      2,    2,     2,   12,   1,    8,     1,     1,   18,
+            16,     1,    3,     6,   1,    1,    1,     3,     48,  1,
+            1,      3,    2,     2,   5,    2,    1,     1,     32,  9,
+            1,      2,    2,     5,   1,    1,    201,   14,    2,   1,
+            1,      9,    8,     2,   1,    2,    1,     2,     1,   1,
+            1,      18,   11184, 27,  49,   1028, 1024,  6942,  1,   737,
+            16,     16,   16,    207, 1,    158,  2,     89,    3,   513,
+            1,      226,  1,     149, 5,    1670, 15,    40,    7,   1,
+            165,    2,    1305,  1,   1,    1,    53,    14,    1,   56,
+            1,      2,    1,     45,  3,    4,    2,     1,     1,   2,
+            1,      66,   3,     36,  5,    1,    6,     2,     62,  1,
+            12,     2,    1,     48,  3,    9,    1,     1,     1,   2,
+            6,      3,    95,    3,   3,    2,    1,     1,     2,   6,
+            1,      160,  1,     3,   7,    1,    21,    2,     2,   56,
+            1,      1,    1,     1,   1,    12,   1,     9,     1,   10,
+            4,      15,   192,   3,   8,    2,    1,     2,     1,   1,
+            105,    1,    2,     6,   1,    1,    2,     1,     1,   2,
+            1,      1,    1,     235, 1,    2,    6,     4,     2,   1,
+            1,      1,    27,    2,   82,   3,    8,     2,     1,   1,
+            1,      1,    106,   1,   1,    1,    2,     6,     1,   1,
+            101,    3,    2,     4,   1,    4,    1,     1283,  1,   14,
+            1,      1,    82,    23,  1,    7,    1,     2,     1,   2,
+            20025,  5,    59,    7,   1050, 62,   4,     19722, 2,   1,
+            4,      5313, 1,     1,   3,    3,    1,     5,     8,   8,
+            2,      7,    30,    4,   148,  3,    1979,  55,    4,   50,
+            8,      1,    14,    1,   22,   1424, 2213,  7,     109, 7,
+            2203,   26,   264,   1,   53,   1,    52,    1,     17,  1,
+            13,     1,    16,    1,   3,    1,    25,    3,     2,   1,
+            2,      3,    30,    1,   1,    1,    13,    5,     66,  2,
+            2,      11,   21,    4,   4,    1,    1,     9,     3,   1,
+            4,      3,    1,     3,   3,    1,    30,    1,     16,  2,
+            106,    1,    4,     1,   71,   2,    4,     1,     21,  1,
+            4,      2,    81,    1,   92,   3,    3,     5,     48,  1,
+            17,     1,    16,    1,   16,   3,    9,     1,     11,  1,
+            587,    5,    1,     1,   7,    1,    9,     10,    3,   2,
+            788162, 31
+          ],
+          [
+            1,  13, 1,  12, 1,  0, 1,  0, 1,  0,  2,  0, 2,  0, 2,  0,  2,  0,
+            2,  0,  2,  0,  2,  0, 3,  0, 2,  0,  1,  0, 2,  0, 2,  0,  2,  3,
+            0,  2,  0,  2,  0,  2, 0,  3, 0,  2,  0,  2, 0,  2, 0,  2,  0,  2,
+            0,  2,  0,  2,  0,  2, 0,  2, 0,  2,  3,  2, 4,  0, 5,  2,  4,  2,
+            0,  4,  2,  4,  6,  4, 0,  2, 5,  0,  2,  0, 5,  0, 2,  4,  0,  5,
+            2,  0,  2,  4,  2,  4, 6,  0, 2,  5,  0,  2, 0,  5, 0,  2,  4,  0,
+            5,  2,  4,  2,  6,  2, 5,  0, 2,  0,  2,  4, 0,  5, 2,  0,  4,  2,
+            4,  6,  0,  2,  0,  2, 4,  0, 5,  2,  0,  2, 4,  2, 4,  6,  2,  5,
+            0,  2,  0,  5,  0,  2, 0,  5, 2,  4,  2,  4, 6,  0, 2,  0,  2,  4,
+            0,  5,  0,  5,  0,  2, 4,  2, 6,  2,  5,  0, 2,  0, 2,  4,  0,  5,
+            2,  0,  4,  2,  4,  2, 4,  2, 4,  2,  6,  2, 5,  0, 2,  0,  2,  4,
+            0,  5,  0,  2,  4,  2, 4,  6, 3,  0,  2,  0, 2,  0, 4,  0,  5,  6,
+            2,  4,  2,  4,  2,  0, 4,  0, 5,  0,  2,  0, 4,  2, 6,  0,  2,  0,
+            5,  0,  2,  0,  4,  2, 0,  2, 0,  5,  0,  2, 0,  2, 0,  2,  0,  2,
+            0,  4,  5,  2,  4,  2, 6,  0, 2,  0,  2,  0, 2,  0, 5,  0,  2,  4,
+            2,  0,  6,  4,  2,  5, 0,  5, 0,  4,  2,  5, 2,  5, 0,  5,  0,  5,
+            2,  5,  2,  0,  4,  2, 0,  2, 5,  0,  2,  0, 7,  8, 9,  0,  2,  0,
+            5,  2,  6,  0,  5,  2, 6,  0, 5,  2,  0,  5, 2,  5, 0,  2,  4,  2,
+            4,  2,  4,  2,  6,  2, 0,  2, 0,  2,  1,  0, 2,  0, 2,  0,  5,  0,
+            2,  4,  2,  4,  2,  4, 2,  0, 5,  0,  5,  0, 5,  2, 4,  2,  0,  5,
+            0,  5,  4,  2,  4,  2, 6,  0, 2,  0,  2,  4, 2,  0, 2,  4,  0,  5,
+            2,  4,  2,  4,  2,  4, 2,  4, 6,  5,  0,  2, 0,  2, 4,  0,  5,  4,
+            2,  4,  2,  6,  2,  5, 0,  5, 0,  5,  0,  2, 4,  2, 4,  2,  4,  2,
+            6,  0,  5,  4,  2,  4, 2,  0, 5,  0,  2,  0, 2,  4, 2,  0,  2,  0,
+            4,  2,  0,  2,  0,  2, 0,  1, 2,  15, 1,  0, 1,  0, 1,  0,  2,  0,
+            16, 0,  17, 0,  17, 0, 17, 0, 16, 0,  17, 0, 16, 0, 17, 0,  2,  0,
+            6,  0,  2,  0,  2,  0, 2,  0, 2,  0,  2,  0, 2,  0, 2,  0,  2,  0,
+            6,  5,  2,  5,  4,  2, 4,  0, 5,  0,  5,  0, 5,  0, 5,  0,  4,  0,
+            5,  4,  6,  2,  0,  2, 0,  5, 0,  2,  0,  5, 2,  4, 6,  0,  7,  2,
+            4,  0,  5,  0,  5,  2, 4,  2, 4,  2,  4,  6, 0,  2, 0,  5,  2,  4,
+            2,  4,  2,  0,  2,  0, 2,  4, 0,  5,  0,  5, 0,  5, 0,  2,  0,  5,
+            2,  0,  2,  0,  2,  0, 2,  0, 2,  0,  5,  4, 2,  4, 0,  4,  6,  0,
+            5,  0,  5,  0,  5,  0, 4,  2, 4,  2,  4,  0, 4,  6, 0,  11, 8,  9,
+            0,  2,  0,  2,  0,  2, 0,  2, 0,  1,  0,  2, 0,  1, 0,  2,  0,  2,
+            0,  2,  0,  2,  0,  2, 6,  0, 2,  0,  4,  2, 4,  0, 2,  6,  0,  6,
+            2,  4,  0,  4,  2,  4, 6,  2, 0,  3,  0,  2, 0,  2, 4,  2,  6,  0,
+            2,  0,  2,  4,  0,  4, 2,  4, 6,  0,  3,  0, 2,  0, 4,  2,  4,  2,
+            6,  2,  0,  2,  0,  2, 4,  2, 6,  0,  2,  4, 0,  2, 0,  2,  4,  2,
+            4,  6,  0,  2,  0,  4, 2,  0, 4,  2,  4,  6, 2,  4, 2,  0,  2,  4,
+            2,  4,  2,  4,  2,  4, 2,  4, 6,  2,  0,  2, 4,  2, 4,  2,  4,  6,
+            2,  0,  2,  0,  4,  2, 4,  2, 4,  6,  2,  0, 2,  4, 2,  4,  2,  6,
+            2,  0,  2,  4,  2,  4, 2,  6, 0,  4,  2,  4, 6,  0, 2,  4,  2,  4,
+            2,  4,  2,  0,  2,  0, 2,  0, 4,  2,  0,  2, 0,  1, 0,  2,  4,  2,
+            0,  4,  2,  1,  2,  0, 2,  0, 2,  0,  2,  0, 2,  0, 2,  0,  2,  0,
+            2,  0,  2,  0,  2,  0, 2,  0, 14, 0,  17, 0, 17, 0, 17, 0,  16, 0,
+            17, 0,  17, 0,  17, 0, 16, 0, 16, 0,  16, 0, 17, 0, 17, 0,  18, 0,
+            16, 0,  16, 0,  19, 0, 16, 0, 16, 0,  16, 0, 16, 0, 16, 0,  17, 0,
+            16, 0,  17, 0,  17, 0, 17, 0, 16, 0,  16, 0, 16, 0, 16, 0,  17, 0,
+            16, 0,  16, 0,  17, 0, 17, 0, 16, 0,  16, 0, 16, 0, 16, 0,  16, 0,
+            16, 0,  16, 0,  16, 0, 16, 0, 1,  2
+          ],
+          true);
+    }
+    return /** @type {number} */ (
+        goog.i18n.GraphemeBreak.inversions_.at(codePoint));
+  }
+};
+
+/**
+ * Extracts a code point from a string at the specified index.
+ *
+ * @param {string} str
+ * @param {number} index
+ * @return {number} Extracted code point.
+ * @private
+ */
+goog.i18n.GraphemeBreak.getCodePoint_ = function(str, index) {
+  var codePoint = goog.i18n.uChar.getCodePointAround(str, index);
+  return (codePoint < 0) ? -codePoint : codePoint;
+};
+
+/**
+ * Indicates if there is a grapheme cluster boundary between a and b.
+ *
+ * Legacy function. Does not cover cases where a sequence of code points is
+ * required in order to decide if there is a grapheme cluster boundary, such as
+ * emoji modifier sequences and emoji flag sequences. To cover all cases please
+ * use {@code hasGraphemeBreakStrings}.
+ *
+ * There are two kinds of grapheme clusters: 1) Legacy 2) Extended. This method
+ * is to check for both using a boolean flag to switch between them. If no flag
+ * is provided rules for the extended clusters will be used by default.
+ *
+ * @param {number} a The code point value of the first character.
+ * @param {number} b The code point value of the second character.
+ * @param {boolean=} opt_extended If true, indicates extended grapheme cluster;
+ *     If false, indicates legacy cluster. Default value is true.
+ * @return {boolean} True if there is a grapheme cluster boundary between
+ *     a and b; False otherwise.
+ */
+goog.i18n.GraphemeBreak.hasGraphemeBreak = function(a, b, opt_extended) {
+  return goog.i18n.GraphemeBreak.applyBreakRules_(a, b, opt_extended !== false);
+};
+
+/**
+ * Indicates if there is a grapheme cluster boundary between a and b.
+ *
+ * There are two kinds of grapheme clusters: 1) Legacy 2) Extended. This method
+ * is to check for both using a boolean flag to switch between them. If no flag
+ * is provided rules for the extended clusters will be used by default.
+ *
+ * @param {string} a String with the first sequence of characters.
+ * @param {string} b String with the second sequence of characters.
+ * @param {boolean=} opt_extended If true, indicates extended grapheme cluster;
+ *     If false, indicates legacy cluster. Default value is true.
+ * @return {boolean} True if there is a grapheme cluster boundary between
+ *     a and b; False otherwise.
+ */
+goog.i18n.GraphemeBreak.hasGraphemeBreakStrings = function(a, b, opt_extended) {
+  goog.asserts.assert(goog.isDef(a), 'First string should be defined.');
+  goog.asserts.assert(goog.isDef(b), 'Second string should be defined.');
+
+  // Break if any of the strings is empty.
+  if (a.length === 0 || b.length === 0) {
+    return true;
+  }
+
+  return goog.i18n.GraphemeBreak.applyBreakRules_(a, b, opt_extended !== false);
+};
diff --git a/third_party/ink/closure/i18n/uchar.js b/third_party/ink/closure/i18n/uchar.js
new file mode 100644
index 0000000..6c22c40
--- /dev/null
+++ b/third_party/ink/closure/i18n/uchar.js
@@ -0,0 +1,294 @@
+// Copyright 2009 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 Collection of utility functions for Unicode character.
+ *
+ * @author cibu@google.com (Cibu Johny)
+ * @author burakov@google.com (Danny Burakov)
+ */
+
+goog.provide('goog.i18n.uChar');
+
+
+// Constants for handling Unicode supplementary characters (surrogate pairs).
+
+
+/**
+ * The minimum value for Supplementary code points.
+ * @type {number}
+ * @private
+ */
+goog.i18n.uChar.SUPPLEMENTARY_CODE_POINT_MIN_VALUE_ = 0x10000;
+
+
+/**
+ * The highest Unicode code point value (scalar value) according to the Unicode
+ * Standard.
+ * @type {number}
+ * @private
+ */
+goog.i18n.uChar.CODE_POINT_MAX_VALUE_ = 0x10FFFF;
+
+
+/**
+ * Lead surrogate minimum value.
+ * @type {number}
+ * @private
+ */
+goog.i18n.uChar.LEAD_SURROGATE_MIN_VALUE_ = 0xD800;
+
+
+/**
+ * Lead surrogate maximum value.
+ * @type {number}
+ * @private
+ */
+goog.i18n.uChar.LEAD_SURROGATE_MAX_VALUE_ = 0xDBFF;
+
+
+/**
+ * Trail surrogate minimum value.
+ * @type {number}
+ * @private
+ */
+goog.i18n.uChar.TRAIL_SURROGATE_MIN_VALUE_ = 0xDC00;
+
+
+/**
+ * Trail surrogate maximum value.
+ * @type {number}
+ * @private
+ */
+goog.i18n.uChar.TRAIL_SURROGATE_MAX_VALUE_ = 0xDFFF;
+
+
+/**
+ * The number of least significant bits of a supplementary code point that in
+ * UTF-16 become the least significant bits of the trail surrogate. The rest of
+ * the in-use bits of the supplementary code point become the least significant
+ * bits of the lead surrogate.
+ * @type {number}
+ * @private
+ */
+goog.i18n.uChar.TRAIL_SURROGATE_BIT_COUNT_ = 10;
+
+
+/**
+ * Gets the U+ notation string of a Unicode character. Ex: 'U+0041' for 'A'.
+ * @param {string} ch The given character.
+ * @return {string} The U+ notation of the given character.
+ */
+goog.i18n.uChar.toHexString = function(ch) {
+  var chCode = goog.i18n.uChar.toCharCode(ch);
+  var chCodeStr = 'U+' +
+      goog.i18n.uChar.padString_(chCode.toString(16).toUpperCase(), 4, '0');
+
+  return chCodeStr;
+};
+
+
+/**
+ * Gets a string padded with given character to get given size.
+ * @param {string} str The given string to be padded.
+ * @param {number} length The target size of the string.
+ * @param {string} ch The character to be padded with.
+ * @return {string} The padded string.
+ * @private
+ */
+goog.i18n.uChar.padString_ = function(str, length, ch) {
+  while (str.length < length) {
+    str = ch + str;
+  }
+  return str;
+};
+
+
+/**
+ * Gets Unicode value of the given character.
+ * @param {string} ch The given character, which in the case of a supplementary
+ * character is actually a surrogate pair. The remainder of the string is
+ * ignored.
+ * @return {number} The Unicode value of the character.
+ */
+goog.i18n.uChar.toCharCode = function(ch) {
+  return goog.i18n.uChar.getCodePointAround(ch, 0);
+};
+
+
+/**
+ * Gets a character from the given Unicode value. If the given code point is not
+ * a valid Unicode code point, null is returned.
+ * @param {number} code The Unicode value of the character.
+ * @return {?string} The character corresponding to the given Unicode value.
+ */
+goog.i18n.uChar.fromCharCode = function(code) {
+  if (!goog.isDefAndNotNull(code) ||
+      !(code >= 0 && code <= goog.i18n.uChar.CODE_POINT_MAX_VALUE_)) {
+    return null;
+  }
+  if (goog.i18n.uChar.isSupplementaryCodePoint(code)) {
+    // First, we split the code point into the trail surrogate part (the
+    // TRAIL_SURROGATE_BIT_COUNT_ least significant bits) and the lead surrogate
+    // part (the rest of the bits, shifted down; note that for now this includes
+    // the supplementary offset, also shifted down, to be subtracted off below).
+    var leadBits = code >> goog.i18n.uChar.TRAIL_SURROGATE_BIT_COUNT_;
+    var trailBits = code &
+        // A bit-mask to get the TRAIL_SURROGATE_BIT_COUNT_ (i.e. 10) least
+        // significant bits. 1 << 10 = 0x0400. 0x0400 - 1 = 0x03FF.
+        ((1 << goog.i18n.uChar.TRAIL_SURROGATE_BIT_COUNT_) - 1);
+
+    // Now we calculate the code point of each surrogate by adding each offset
+    // to the corresponding base code point.
+    var leadCodePoint = leadBits +
+        (goog.i18n.uChar.LEAD_SURROGATE_MIN_VALUE_ -
+         // Subtract off the supplementary offset, which had been shifted down
+         // with the rest of leadBits. We do this here instead of before the
+         // shift in order to save a separate subtraction step.
+         (goog.i18n.uChar.SUPPLEMENTARY_CODE_POINT_MIN_VALUE_ >>
+          goog.i18n.uChar.TRAIL_SURROGATE_BIT_COUNT_));
+    var trailCodePoint = trailBits + goog.i18n.uChar.TRAIL_SURROGATE_MIN_VALUE_;
+
+    // Convert the code points into a 2-character long string.
+    return String.fromCharCode(leadCodePoint) +
+        String.fromCharCode(trailCodePoint);
+  }
+  return String.fromCharCode(code);
+};
+
+
+/**
+ * Returns the Unicode code point at the specified index.
+ *
+ * If the char value specified at the given index is in the leading-surrogate
+ * range, and the following index is less than the length of {@code string}, and
+ * the char value at the following index is in the trailing-surrogate range,
+ * then the supplementary code point corresponding to this surrogate pair is
+ * returned.
+ *
+ * If the char value specified at the given index is in the trailing-surrogate
+ * range, and the preceding index is not before the start of {@code string}, and
+ * the char value at the preceding index is in the leading-surrogate range, then
+ * the negated supplementary code point corresponding to this surrogate pair is
+ * returned.
+ *
+ * The negation allows the caller to differentiate between the case where the
+ * given index is at the leading surrogate and the one where it is at the
+ * trailing surrogate, and thus deduce where the next character starts and
+ * preceding character ends.
+ *
+ * Otherwise, the char value at the given index is returned. Thus, a leading
+ * surrogate is returned when it is not followed by a trailing surrogate, and a
+ * trailing surrogate is returned when it is not preceded by a leading
+ * surrogate.
+ *
+ * @param {string} string The string.
+ * @param {number} index The index from which the code point is to be retrieved.
+ * @return {number} The code point at the given index. If the given index is
+ * that of the start (i.e. lead surrogate) of a surrogate pair, returns the code
+ * point encoded by the pair. If the given index is that of the end (i.e. trail
+ * surrogate) of a surrogate pair, returns the negated code pointed encoded by
+ * the pair.
+ */
+goog.i18n.uChar.getCodePointAround = function(string, index) {
+  var charCode = string.charCodeAt(index);
+  if (goog.i18n.uChar.isLeadSurrogateCodePoint(charCode) &&
+      index + 1 < string.length) {
+    var trail = string.charCodeAt(index + 1);
+    if (goog.i18n.uChar.isTrailSurrogateCodePoint(trail)) {
+      // Part of a surrogate pair.
+      return /** @type {number} */ (
+          goog.i18n.uChar.buildSupplementaryCodePoint(charCode, trail));
+    }
+  } else if (goog.i18n.uChar.isTrailSurrogateCodePoint(charCode) && index > 0) {
+    var lead = string.charCodeAt(index - 1);
+    if (goog.i18n.uChar.isLeadSurrogateCodePoint(lead)) {
+      // Part of a surrogate pair.
+      return /** @type {number} */ (
+          -goog.i18n.uChar.buildSupplementaryCodePoint(lead, charCode));
+    }
+  }
+  return charCode;
+};
+
+
+/**
+ * Determines the length of the string needed to represent the specified
+ * Unicode code point.
+ * @param {number} codePoint
+ * @return {number} 2 if codePoint is a supplementary character, 1 otherwise.
+ */
+goog.i18n.uChar.charCount = function(codePoint) {
+  return goog.i18n.uChar.isSupplementaryCodePoint(codePoint) ? 2 : 1;
+};
+
+
+/**
+ * Determines whether the specified Unicode code point is in the supplementary
+ * Unicode characters range.
+ * @param {number} codePoint
+ * @return {boolean} Whether then given code point is a supplementary character.
+ */
+goog.i18n.uChar.isSupplementaryCodePoint = function(codePoint) {
+  return codePoint >= goog.i18n.uChar.SUPPLEMENTARY_CODE_POINT_MIN_VALUE_ &&
+      codePoint <= goog.i18n.uChar.CODE_POINT_MAX_VALUE_;
+};
+
+
+/**
+ * Gets whether the given code point is a leading surrogate character.
+ * @param {number} codePoint
+ * @return {boolean} Whether the given code point is a leading surrogate
+ * character.
+ */
+goog.i18n.uChar.isLeadSurrogateCodePoint = function(codePoint) {
+  return codePoint >= goog.i18n.uChar.LEAD_SURROGATE_MIN_VALUE_ &&
+      codePoint <= goog.i18n.uChar.LEAD_SURROGATE_MAX_VALUE_;
+};
+
+
+/**
+ * Gets whether the given code point is a trailing surrogate character.
+ * @param {number} codePoint
+ * @return {boolean} Whether the given code point is a trailing surrogate
+ * character.
+ */
+goog.i18n.uChar.isTrailSurrogateCodePoint = function(codePoint) {
+  return codePoint >= goog.i18n.uChar.TRAIL_SURROGATE_MIN_VALUE_ &&
+      codePoint <= goog.i18n.uChar.TRAIL_SURROGATE_MAX_VALUE_;
+};
+
+
+/**
+ * Composes a supplementary Unicode code point from the given UTF-16 surrogate
+ * pair. If leadSurrogate isn't a leading surrogate code point or trailSurrogate
+ * isn't a trailing surrogate code point, null is returned.
+ * @param {number} lead The leading surrogate code point.
+ * @param {number} trail The trailing surrogate code point.
+ * @return {?number} The supplementary Unicode code point obtained by decoding
+ * the given UTF-16 surrogate pair.
+ */
+goog.i18n.uChar.buildSupplementaryCodePoint = function(lead, trail) {
+  if (goog.i18n.uChar.isLeadSurrogateCodePoint(lead) &&
+      goog.i18n.uChar.isTrailSurrogateCodePoint(trail)) {
+    var shiftedLeadOffset =
+        (lead << goog.i18n.uChar.TRAIL_SURROGATE_BIT_COUNT_) -
+        (goog.i18n.uChar.LEAD_SURROGATE_MIN_VALUE_
+         << goog.i18n.uChar.TRAIL_SURROGATE_BIT_COUNT_);
+    var trailOffset = trail - goog.i18n.uChar.TRAIL_SURROGATE_MIN_VALUE_ +
+        goog.i18n.uChar.SUPPLEMENTARY_CODE_POINT_MIN_VALUE_;
+    return shiftedLeadOffset + trailOffset;
+  }
+  return null;
+};
diff --git a/third_party/ink/closure/iter/iter.js b/third_party/ink/closure/iter/iter.js
new file mode 100644
index 0000000..534d24ad
--- /dev/null
+++ b/third_party/ink/closure/iter/iter.js
@@ -0,0 +1,1284 @@
+// Copyright 2007 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 Python style iteration utilities.
+ * @author arv@google.com (Erik Arvidsson)
+ */
+
+
+goog.provide('goog.iter');
+goog.provide('goog.iter.Iterable');
+goog.provide('goog.iter.Iterator');
+goog.provide('goog.iter.StopIteration');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.functions');
+goog.require('goog.math');
+
+
+/**
+ * @typedef {goog.iter.Iterator|{length:number}|{__iterator__}}
+ */
+goog.iter.Iterable;
+
+
+/**
+ * Singleton Error object that is used to terminate iterations.
+ * @const {!Error}
+ */
+goog.iter.StopIteration = ('StopIteration' in goog.global) ?
+    // For script engines that support legacy iterators.
+    goog.global['StopIteration'] :
+    {message: 'StopIteration', stack: ''};
+
+
+
+/**
+ * Class/interface for iterators.  An iterator needs to implement a {@code next}
+ * method and it needs to throw a {@code goog.iter.StopIteration} when the
+ * iteration passes beyond the end.  Iterators have no {@code hasNext} method.
+ * It is recommended to always use the helper functions to iterate over the
+ * iterator or in case you are only targeting JavaScript 1.7 for in loops.
+ * @constructor
+ * @template VALUE
+ */
+goog.iter.Iterator = function() {};
+
+
+/**
+ * Returns the next value of the iteration.  This will throw the object
+ * {@see goog.iter#StopIteration} when the iteration passes the end.
+ * @return {VALUE} Any object or value.
+ */
+goog.iter.Iterator.prototype.next = function() {
+  throw goog.iter.StopIteration;
+};
+
+
+/**
+ * Returns the {@code Iterator} object itself.  This is used to implement
+ * the iterator protocol in JavaScript 1.7
+ * @param {boolean=} opt_keys  Whether to return the keys or values. Default is
+ *     to only return the values.  This is being used by the for-in loop (true)
+ *     and the for-each-in loop (false).  Even though the param gives a hint
+ *     about what the iterator will return there is no guarantee that it will
+ *     return the keys when true is passed.
+ * @return {!goog.iter.Iterator<VALUE>} The object itself.
+ */
+goog.iter.Iterator.prototype.__iterator__ = function(opt_keys) {
+  return this;
+};
+
+
+/**
+ * Returns an iterator that knows how to iterate over the values in the object.
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable  If the
+ *     object is an iterator it will be returned as is.  If the object has an
+ *     {@code __iterator__} method that will be called to get the value
+ *     iterator.  If the object is an array-like object we create an iterator
+ *     for that.
+ * @return {!goog.iter.Iterator<VALUE>} An iterator that knows how to iterate
+ *     over the values in {@code iterable}.
+ * @template VALUE
+ */
+goog.iter.toIterator = function(iterable) {
+  if (iterable instanceof goog.iter.Iterator) {
+    return iterable;
+  }
+  if (typeof iterable.__iterator__ == 'function') {
+    return iterable.__iterator__(false);
+  }
+  if (goog.isArrayLike(iterable)) {
+    var i = 0;
+    var newIter = new goog.iter.Iterator;
+    newIter.next = function() {
+      while (true) {
+        if (i >= iterable.length) {
+          throw goog.iter.StopIteration;
+        }
+        // Don't include deleted elements.
+        if (!(i in iterable)) {
+          i++;
+          continue;
+        }
+        return iterable[i++];
+      }
+    };
+    return newIter;
+  }
+
+
+  // TODO(arv): Should we fall back on goog.structs.getValues()?
+  throw new Error('Not implemented');
+};
+
+
+/**
+ * Calls a function for each element in the iterator with the element of the
+ * iterator passed as argument.
+ *
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable  The iterator
+ *     to iterate over. If the iterable is an object {@code toIterator} will be
+ *     called on it.
+ * @param {function(this:THIS,VALUE,?,!goog.iter.Iterator<VALUE>)} f
+ *     The function to call for every element.  This function takes 3 arguments
+ *     (the element, undefined, and the iterator) and the return value is
+ *     irrelevant.  The reason for passing undefined as the second argument is
+ *     so that the same function can be used in {@see goog.array#forEach} as
+ *     well as others.  The third parameter is of type "number" for
+ *     arraylike objects, undefined, otherwise.
+ * @param {THIS=} opt_obj  The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @template THIS, VALUE
+ */
+goog.iter.forEach = function(iterable, f, opt_obj) {
+  if (goog.isArrayLike(iterable)) {
+
+    try {
+      // NOTES: this passes the index number to the second parameter
+      // of the callback contrary to the documentation above.
+      goog.array.forEach(
+          /** @type {IArrayLike<?>} */ (iterable), f, opt_obj);
+    } catch (ex) {
+      if (ex !== goog.iter.StopIteration) {
+        throw ex;
+      }
+    }
+  } else {
+    iterable = goog.iter.toIterator(iterable);
+
+    try {
+      while (true) {
+        f.call(opt_obj, iterable.next(), undefined, iterable);
+      }
+    } catch (ex) {
+      if (ex !== goog.iter.StopIteration) {
+        throw ex;
+      }
+    }
+  }
+};
+
+
+/**
+ * Calls a function for every element in the iterator, and if the function
+ * returns true adds the element to a new iterator.
+ *
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     to iterate over.
+ * @param {
+ *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
+ *     The function to call for every element. This function takes 3 arguments
+ *     (the element, undefined, and the iterator) and should return a boolean.
+ *     If the return value is true the element will be included in the returned
+ *     iterator.  If it is false the element is not included.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator in which only elements
+ *     that passed the test are present.
+ * @template THIS, VALUE
+ */
+goog.iter.filter = function(iterable, f, opt_obj) {
+  var iterator = goog.iter.toIterator(iterable);
+  var newIter = new goog.iter.Iterator;
+  newIter.next = function() {
+    while (true) {
+      var val = iterator.next();
+      if (f.call(opt_obj, val, undefined, iterator)) {
+        return val;
+      }
+    }
+  };
+  return newIter;
+};
+
+
+/**
+ * Calls a function for every element in the iterator, and if the function
+ * returns false adds the element to a new iterator.
+ *
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     to iterate over.
+ * @param {
+ *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
+ *     The function to call for every element. This function takes 3 arguments
+ *     (the element, undefined, and the iterator) and should return a boolean.
+ *     If the return value is false the element will be included in the returned
+ *     iterator.  If it is true the element is not included.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator in which only elements
+ *     that did not pass the test are present.
+ * @template THIS, VALUE
+ */
+goog.iter.filterFalse = function(iterable, f, opt_obj) {
+  return goog.iter.filter(iterable, goog.functions.not(f), opt_obj);
+};
+
+
+/**
+ * Creates a new iterator that returns the values in a range.  This function
+ * can take 1, 2 or 3 arguments:
+ * <pre>
+ * range(5) same as range(0, 5, 1)
+ * range(2, 5) same as range(2, 5, 1)
+ * </pre>
+ *
+ * @param {number} startOrStop  The stop value if only one argument is provided.
+ *     The start value if 2 or more arguments are provided.  If only one
+ *     argument is used the start value is 0.
+ * @param {number=} opt_stop  The stop value.  If left out then the first
+ *     argument is used as the stop value.
+ * @param {number=} opt_step  The number to increment with between each call to
+ *     next.  This can be negative.
+ * @return {!goog.iter.Iterator<number>} A new iterator that returns the values
+ *     in the range.
+ */
+goog.iter.range = function(startOrStop, opt_stop, opt_step) {
+  var start = 0;
+  var stop = startOrStop;
+  var step = opt_step || 1;
+  if (arguments.length > 1) {
+    start = startOrStop;
+    stop = opt_stop;
+  }
+  if (step == 0) {
+    throw new Error('Range step argument must not be zero');
+  }
+
+  var newIter = new goog.iter.Iterator;
+  newIter.next = function() {
+    if (step > 0 && start >= stop || step < 0 && start <= stop) {
+      throw goog.iter.StopIteration;
+    }
+    var rv = start;
+    start += step;
+    return rv;
+  };
+  return newIter;
+};
+
+
+/**
+ * Joins the values in a iterator with a delimiter.
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     to get the values from.
+ * @param {string} deliminator  The text to put between the values.
+ * @return {string} The joined value string.
+ * @template VALUE
+ */
+goog.iter.join = function(iterable, deliminator) {
+  return goog.iter.toArray(iterable).join(deliminator);
+};
+
+
+/**
+ * For every element in the iterator call a function and return a new iterator
+ * with that value.
+ *
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterator to iterate over.
+ * @param {
+ *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):RESULT} f
+ *     The function to call for every element.  This function takes 3 arguments
+ *     (the element, undefined, and the iterator) and should return a new value.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @return {!goog.iter.Iterator<RESULT>} A new iterator that returns the
+ *     results of applying the function to each element in the original
+ *     iterator.
+ * @template THIS, VALUE, RESULT
+ */
+goog.iter.map = function(iterable, f, opt_obj) {
+  var iterator = goog.iter.toIterator(iterable);
+  var newIter = new goog.iter.Iterator;
+  newIter.next = function() {
+    var val = iterator.next();
+    return f.call(opt_obj, val, undefined, iterator);
+  };
+  return newIter;
+};
+
+
+/**
+ * Passes every element of an iterator into a function and accumulates the
+ * result.
+ *
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     to iterate over.
+ * @param {function(this:THIS,VALUE,VALUE):VALUE} f The function to call for
+ *     every element. This function takes 2 arguments (the function's previous
+ *     result or the initial value, and the value of the current element).
+ *     function(previousValue, currentElement) : newValue.
+ * @param {VALUE} val The initial value to pass into the function on the first
+ *     call.
+ * @param {THIS=} opt_obj  The object to be used as the value of 'this' within
+ *     f.
+ * @return {VALUE} Result of evaluating f repeatedly across the values of
+ *     the iterator.
+ * @template THIS, VALUE
+ */
+goog.iter.reduce = function(iterable, f, val, opt_obj) {
+  var rval = val;
+  goog.iter.forEach(
+      iterable, function(val) { rval = f.call(opt_obj, rval, val); });
+  return rval;
+};
+
+
+/**
+ * Goes through the values in the iterator. Calls f for each of these, and if
+ * any of them returns true, this returns true (without checking the rest). If
+ * all return false this will return false.
+ *
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     object.
+ * @param {
+ *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
+ *     The function to call for every value. This function takes 3 arguments
+ *     (the value, undefined, and the iterator) and should return a boolean.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @return {boolean} true if any value passes the test.
+ * @template THIS, VALUE
+ */
+goog.iter.some = function(iterable, f, opt_obj) {
+  iterable = goog.iter.toIterator(iterable);
+
+  try {
+    while (true) {
+      if (f.call(opt_obj, iterable.next(), undefined, iterable)) {
+        return true;
+      }
+    }
+  } catch (ex) {
+    if (ex !== goog.iter.StopIteration) {
+      throw ex;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Goes through the values in the iterator. Calls f for each of these and if any
+ * of them returns false this returns false (without checking the rest). If all
+ * return true this will return true.
+ *
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     object.
+ * @param {
+ *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
+ *     The function to call for every value. This function takes 3 arguments
+ *     (the value, undefined, and the iterator) and should return a boolean.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @return {boolean} true if every value passes the test.
+ * @template THIS, VALUE
+ */
+goog.iter.every = function(iterable, f, opt_obj) {
+  iterable = goog.iter.toIterator(iterable);
+
+  try {
+    while (true) {
+      if (!f.call(opt_obj, iterable.next(), undefined, iterable)) {
+        return false;
+      }
+    }
+  } catch (ex) {
+    if (ex !== goog.iter.StopIteration) {
+      throw ex;
+    }
+  }
+  return true;
+};
+
+
+/**
+ * Takes zero or more iterables and returns one iterator that will iterate over
+ * them in the order chained.
+ * @param {...!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} var_args Any
+ *     number of iterable objects.
+ * @return {!goog.iter.Iterator<VALUE>} Returns a new iterator that will
+ *     iterate over all the given iterables' contents.
+ * @template VALUE
+ */
+goog.iter.chain = function(var_args) {
+  return goog.iter.chainFromIterable(arguments);
+};
+
+
+/**
+ * Takes a single iterable containing zero or more iterables and returns one
+ * iterator that will iterate over each one in the order given.
+ * @see https://goo.gl/5NRp5d
+ * @param {goog.iter.Iterable} iterable The iterable of iterables to chain.
+ * @return {!goog.iter.Iterator<VALUE>} Returns a new iterator that will
+ *     iterate over all the contents of the iterables contained within
+ *     {@code iterable}.
+ * @template VALUE
+ */
+goog.iter.chainFromIterable = function(iterable) {
+  var iterator = goog.iter.toIterator(iterable);
+  var iter = new goog.iter.Iterator();
+  var current = null;
+
+  iter.next = function() {
+    while (true) {
+      if (current == null) {
+        var it = iterator.next();
+        current = goog.iter.toIterator(it);
+      }
+      try {
+        return current.next();
+      } catch (ex) {
+        if (ex !== goog.iter.StopIteration) {
+          throw ex;
+        }
+        current = null;
+      }
+    }
+  };
+
+  return iter;
+};
+
+
+/**
+ * Builds a new iterator that iterates over the original, but skips elements as
+ * long as a supplied function returns true.
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     object.
+ * @param {
+ *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
+ *     The function to call for every value. This function takes 3 arguments
+ *     (the value, undefined, and the iterator) and should return a boolean.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator that drops elements from
+ *     the original iterator as long as {@code f} is true.
+ * @template THIS, VALUE
+ */
+goog.iter.dropWhile = function(iterable, f, opt_obj) {
+  var iterator = goog.iter.toIterator(iterable);
+  var newIter = new goog.iter.Iterator;
+  var dropping = true;
+  newIter.next = function() {
+    while (true) {
+      var val = iterator.next();
+      if (dropping && f.call(opt_obj, val, undefined, iterator)) {
+        continue;
+      } else {
+        dropping = false;
+      }
+      return val;
+    }
+  };
+  return newIter;
+};
+
+
+/**
+ * Builds a new iterator that iterates over the original, but only as long as a
+ * supplied function returns true.
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     object.
+ * @param {
+ *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
+ *     The function to call for every value. This function takes 3 arguments
+ *     (the value, undefined, and the iterator) and should return a boolean.
+ * @param {THIS=} opt_obj This is used as the 'this' object in f when called.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator that keeps elements in
+ *     the original iterator as long as the function is true.
+ * @template THIS, VALUE
+ */
+goog.iter.takeWhile = function(iterable, f, opt_obj) {
+  var iterator = goog.iter.toIterator(iterable);
+  var iter = new goog.iter.Iterator();
+  iter.next = function() {
+    var val = iterator.next();
+    if (f.call(opt_obj, val, undefined, iterator)) {
+      return val;
+    }
+    throw goog.iter.StopIteration;
+  };
+  return iter;
+};
+
+
+/**
+ * Converts the iterator to an array
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     to convert to an array.
+ * @return {!Array<VALUE>} An array of the elements the iterator iterates over.
+ * @template VALUE
+ */
+goog.iter.toArray = function(iterable) {
+  // Fast path for array-like.
+  if (goog.isArrayLike(iterable)) {
+    return goog.array.toArray(/** @type {!IArrayLike<?>} */ (iterable));
+  }
+  iterable = goog.iter.toIterator(iterable);
+  var array = [];
+  goog.iter.forEach(iterable, function(val) { array.push(val); });
+  return array;
+};
+
+
+/**
+ * Iterates over two iterables and returns true if they contain the same
+ * sequence of elements and have the same length.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable1 The first
+ *     iterable object.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable2 The second
+ *     iterable object.
+ * @param {function(VALUE,VALUE):boolean=} opt_equalsFn Optional comparison
+ *     function.
+ *     Should take two arguments to compare, and return true if the arguments
+ *     are equal. Defaults to {@link goog.array.defaultCompareEquality} which
+ *     compares the elements using the built-in '===' operator.
+ * @return {boolean} true if the iterables contain the same sequence of elements
+ *     and have the same length.
+ * @template VALUE
+ */
+goog.iter.equals = function(iterable1, iterable2, opt_equalsFn) {
+  var fillValue = {};
+  var pairs = goog.iter.zipLongest(fillValue, iterable1, iterable2);
+  var equalsFn = opt_equalsFn || goog.array.defaultCompareEquality;
+  return goog.iter.every(
+      pairs, function(pair) { return equalsFn(pair[0], pair[1]); });
+};
+
+
+/**
+ * Advances the iterator to the next position, returning the given default value
+ * instead of throwing an exception if the iterator has no more entries.
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterable
+ *     object.
+ * @param {VALUE} defaultValue The value to return if the iterator is empty.
+ * @return {VALUE} The next item in the iteration, or defaultValue if the
+ *     iterator was empty.
+ * @template VALUE
+ */
+goog.iter.nextOrValue = function(iterable, defaultValue) {
+  try {
+    return goog.iter.toIterator(iterable).next();
+  } catch (e) {
+    if (e != goog.iter.StopIteration) {
+      throw e;
+    }
+    return defaultValue;
+  }
+};
+
+
+/**
+ * Cartesian product of zero or more sets.  Gives an iterator that gives every
+ * combination of one element chosen from each set.  For example,
+ * ([1, 2], [3, 4]) gives ([1, 3], [1, 4], [2, 3], [2, 4]).
+ * @see http://docs.python.org/library/itertools.html#itertools.product
+ * @param {...!IArrayLike<VALUE>} var_args Zero or more sets, as
+ *     arrays.
+ * @return {!goog.iter.Iterator<!Array<VALUE>>} An iterator that gives each
+ *     n-tuple (as an array).
+ * @template VALUE
+ */
+goog.iter.product = function(var_args) {
+  var someArrayEmpty =
+      goog.array.some(arguments, function(arr) { return !arr.length; });
+
+  // An empty set in a cartesian product gives an empty set.
+  if (someArrayEmpty || !arguments.length) {
+    return new goog.iter.Iterator();
+  }
+
+  var iter = new goog.iter.Iterator();
+  var arrays = arguments;
+
+  // The first indices are [0, 0, ...]
+  var indicies = goog.array.repeat(0, arrays.length);
+
+  iter.next = function() {
+
+    if (indicies) {
+      var retVal = goog.array.map(indicies, function(valueIndex, arrayIndex) {
+        return arrays[arrayIndex][valueIndex];
+      });
+
+      // Generate the next-largest indices for the next call.
+      // Increase the rightmost index. If it goes over, increase the next
+      // rightmost (like carry-over addition).
+      for (var i = indicies.length - 1; i >= 0; i--) {
+        // Assertion prevents compiler warning below.
+        goog.asserts.assert(indicies);
+        if (indicies[i] < arrays[i].length - 1) {
+          indicies[i]++;
+          break;
+        }
+
+        // We're at the last indices (the last element of every array), so
+        // the iteration is over on the next call.
+        if (i == 0) {
+          indicies = null;
+          break;
+        }
+        // Reset the index in this column and loop back to increment the
+        // next one.
+        indicies[i] = 0;
+      }
+      return retVal;
+    }
+
+    throw goog.iter.StopIteration;
+  };
+
+  return iter;
+};
+
+
+/**
+ * Create an iterator to cycle over the iterable's elements indefinitely.
+ * For example, ([1, 2, 3]) would return : 1, 2, 3, 1, 2, 3, ...
+ * @see: http://docs.python.org/library/itertools.html#itertools.cycle.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable object.
+ * @return {!goog.iter.Iterator<VALUE>} An iterator that iterates indefinitely
+ *     over the values in {@code iterable}.
+ * @template VALUE
+ */
+goog.iter.cycle = function(iterable) {
+  var baseIterator = goog.iter.toIterator(iterable);
+
+  // We maintain a cache to store the iterable elements as we iterate
+  // over them. The cache is used to return elements once we have
+  // iterated over the iterable once.
+  var cache = [];
+  var cacheIndex = 0;
+
+  var iter = new goog.iter.Iterator();
+
+  // This flag is set after the iterable is iterated over once
+  var useCache = false;
+
+  iter.next = function() {
+    var returnElement = null;
+
+    // Pull elements off the original iterator if not using cache
+    if (!useCache) {
+      try {
+        // Return the element from the iterable
+        returnElement = baseIterator.next();
+        cache.push(returnElement);
+        return returnElement;
+      } catch (e) {
+        // If an exception other than StopIteration is thrown
+        // or if there are no elements to iterate over (the iterable was empty)
+        // throw an exception
+        if (e != goog.iter.StopIteration || goog.array.isEmpty(cache)) {
+          throw e;
+        }
+        // set useCache to true after we know that a 'StopIteration' exception
+        // was thrown and the cache is not empty (to handle the 'empty iterable'
+        // use case)
+        useCache = true;
+      }
+    }
+
+    returnElement = cache[cacheIndex];
+    cacheIndex = (cacheIndex + 1) % cache.length;
+
+    return returnElement;
+  };
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that counts indefinitely from a starting value.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.count
+ * @param {number=} opt_start The starting value. Default is 0.
+ * @param {number=} opt_step The number to increment with between each call to
+ *     next. Negative and floating point numbers are allowed. Default is 1.
+ * @return {!goog.iter.Iterator<number>} A new iterator that returns the values
+ *     in the series.
+ */
+goog.iter.count = function(opt_start, opt_step) {
+  var counter = opt_start || 0;
+  var step = goog.isDef(opt_step) ? opt_step : 1;
+  var iter = new goog.iter.Iterator();
+
+  iter.next = function() {
+    var returnValue = counter;
+    counter += step;
+    return returnValue;
+  };
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that returns the same object or value repeatedly.
+ * @param {VALUE} value Any object or value to repeat.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator that returns the
+ *     repeated value.
+ * @template VALUE
+ */
+goog.iter.repeat = function(value) {
+  var iter = new goog.iter.Iterator();
+
+  iter.next = goog.functions.constant(value);
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that returns running totals from the numbers in
+ * {@code iterable}. For example, the array {@code [1, 2, 3, 4, 5]} yields
+ * {@code 1 -> 3 -> 6 -> 10 -> 15}.
+ * @see http://docs.python.org/3.2/library/itertools.html#itertools.accumulate
+ * @param {!goog.iter.Iterable} iterable The iterable of numbers to
+ *     accumulate.
+ * @return {!goog.iter.Iterator<number>} A new iterator that returns the
+ *     numbers in the series.
+ */
+goog.iter.accumulate = function(iterable) {
+  var iterator = goog.iter.toIterator(iterable);
+  var total = 0;
+  var iter = new goog.iter.Iterator();
+
+  iter.next = function() {
+    total += iterator.next();
+    return total;
+  };
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that returns arrays containing the ith elements from the
+ * provided iterables. The returned arrays will be the same size as the number
+ * of iterables given in {@code var_args}. Once the shortest iterable is
+ * exhausted, subsequent calls to {@code next()} will throw
+ * {@code goog.iter.StopIteration}.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.izip
+ * @param {...!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} var_args Any
+ *     number of iterable objects.
+ * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator that returns
+ *     arrays of elements from the provided iterables.
+ * @template VALUE
+ */
+goog.iter.zip = function(var_args) {
+  var args = arguments;
+  var iter = new goog.iter.Iterator();
+
+  if (args.length > 0) {
+    var iterators = goog.array.map(args, goog.iter.toIterator);
+    iter.next = function() {
+      var arr = goog.array.map(iterators, function(it) { return it.next(); });
+      return arr;
+    };
+  }
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that returns arrays containing the ith elements from the
+ * provided iterables. The returned arrays will be the same size as the number
+ * of iterables given in {@code var_args}. Shorter iterables will be extended
+ * with {@code fillValue}. Once the longest iterable is exhausted, subsequent
+ * calls to {@code next()} will throw {@code goog.iter.StopIteration}.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.izip_longest
+ * @param {VALUE} fillValue The object or value used to fill shorter iterables.
+ * @param {...!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} var_args Any
+ *     number of iterable objects.
+ * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator that returns
+ *     arrays of elements from the provided iterables.
+ * @template VALUE
+ */
+goog.iter.zipLongest = function(fillValue, var_args) {
+  var args = goog.array.slice(arguments, 1);
+  var iter = new goog.iter.Iterator();
+
+  if (args.length > 0) {
+    var iterators = goog.array.map(args, goog.iter.toIterator);
+
+    iter.next = function() {
+      var iteratorsHaveValues = false;  // false when all iterators are empty.
+      var arr = goog.array.map(iterators, function(it) {
+        var returnValue;
+        try {
+          returnValue = it.next();
+          // Iterator had a value, so we've not exhausted the iterators.
+          // Set flag accordingly.
+          iteratorsHaveValues = true;
+        } catch (ex) {
+          if (ex !== goog.iter.StopIteration) {
+            throw ex;
+          }
+          returnValue = fillValue;
+        }
+        return returnValue;
+      });
+
+      if (!iteratorsHaveValues) {
+        throw goog.iter.StopIteration;
+      }
+      return arr;
+    };
+  }
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that filters {@code iterable} based on a series of
+ * {@code selectors}. On each call to {@code next()}, one item is taken from
+ * both the {@code iterable} and {@code selectors} iterators. If the item from
+ * {@code selectors} evaluates to true, the item from {@code iterable} is given.
+ * Otherwise, it is skipped. Once either {@code iterable} or {@code selectors}
+ * is exhausted, subsequent calls to {@code next()} will throw
+ * {@code goog.iter.StopIteration}.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.compress
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to filter.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} selectors An
+ *     iterable of items to be evaluated in a boolean context to determine if
+ *     the corresponding element in {@code iterable} should be included in the
+ *     result.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator that returns the
+ *     filtered values.
+ * @template VALUE
+ */
+goog.iter.compress = function(iterable, selectors) {
+  var selectorIterator = goog.iter.toIterator(selectors);
+
+  return goog.iter.filter(
+      iterable, function() { return !!selectorIterator.next(); });
+};
+
+
+
+/**
+ * Implements the {@code goog.iter.groupBy} iterator.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to group.
+ * @param {function(VALUE): KEY=} opt_keyFunc  Optional function for
+ *     determining the key value for each group in the {@code iterable}. Default
+ *     is the identity function.
+ * @constructor
+ * @extends {goog.iter.Iterator<!Array<?>>}
+ * @template KEY, VALUE
+ * @private
+ */
+goog.iter.GroupByIterator_ = function(iterable, opt_keyFunc) {
+
+  /**
+   * The iterable to group, coerced to an iterator.
+   * @type {!goog.iter.Iterator}
+   */
+  this.iterator = goog.iter.toIterator(iterable);
+
+  /**
+   * A function for determining the key value for each element in the iterable.
+   * If no function is provided, the identity function is used and returns the
+   * element unchanged.
+   * @type {function(VALUE): KEY}
+   */
+  this.keyFunc = opt_keyFunc || goog.functions.identity;
+
+  /**
+   * The target key for determining the start of a group.
+   * @type {KEY}
+   */
+  this.targetKey;
+
+  /**
+   * The current key visited during iteration.
+   * @type {KEY}
+   */
+  this.currentKey;
+
+  /**
+   * The current value being added to the group.
+   * @type {VALUE}
+   */
+  this.currentValue;
+};
+goog.inherits(goog.iter.GroupByIterator_, goog.iter.Iterator);
+
+
+/** @override */
+goog.iter.GroupByIterator_.prototype.next = function() {
+  while (this.currentKey == this.targetKey) {
+    this.currentValue = this.iterator.next();  // Exits on StopIteration
+    this.currentKey = this.keyFunc(this.currentValue);
+  }
+  this.targetKey = this.currentKey;
+  return [this.currentKey, this.groupItems_(this.targetKey)];
+};
+
+
+/**
+ * Performs the grouping of objects using the given key.
+ * @param {KEY} targetKey  The target key object for the group.
+ * @return {!Array<VALUE>} An array of grouped objects.
+ * @private
+ */
+goog.iter.GroupByIterator_.prototype.groupItems_ = function(targetKey) {
+  var arr = [];
+  while (this.currentKey == targetKey) {
+    arr.push(this.currentValue);
+    try {
+      this.currentValue = this.iterator.next();
+    } catch (ex) {
+      if (ex !== goog.iter.StopIteration) {
+        throw ex;
+      }
+      break;
+    }
+    this.currentKey = this.keyFunc(this.currentValue);
+  }
+  return arr;
+};
+
+
+/**
+ * Creates an iterator that returns arrays containing elements from the
+ * {@code iterable} grouped by a key value. For iterables with repeated
+ * elements (i.e. sorted according to a particular key function), this function
+ * has a {@code uniq}-like effect. For example, grouping the array:
+ * {@code [A, B, B, C, C, A]} produces
+ * {@code [A, [A]], [B, [B, B]], [C, [C, C]], [A, [A]]}.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.groupby
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to group.
+ * @param {function(VALUE): KEY=} opt_keyFunc  Optional function for
+ *     determining the key value for each group in the {@code iterable}. Default
+ *     is the identity function.
+ * @return {!goog.iter.Iterator<!Array<?>>} A new iterator that returns
+ *     arrays of consecutive key and groups.
+ * @template KEY, VALUE
+ */
+goog.iter.groupBy = function(iterable, opt_keyFunc) {
+  return new goog.iter.GroupByIterator_(iterable, opt_keyFunc);
+};
+
+
+/**
+ * Gives an iterator that gives the result of calling the given function
+ * <code>f</code> with the arguments taken from the next element from
+ * <code>iterable</code> (the elements are expected to also be iterables).
+ *
+ * Similar to {@see goog.iter#map} but allows the function to accept multiple
+ * arguments from the iterable.
+ *
+ * @param {!goog.iter.Iterable} iterable The iterable of
+ *     iterables to iterate over.
+ * @param {function(this:THIS,...*):RESULT} f The function to call for every
+ *     element.  This function takes N+2 arguments, where N represents the
+ *     number of items from the next element of the iterable. The two
+ *     additional arguments passed to the function are undefined and the
+ *     iterator itself. The function should return a new value.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @return {!goog.iter.Iterator<RESULT>} A new iterator that returns the
+ *     results of applying the function to each element in the original
+ *     iterator.
+ * @template THIS, RESULT
+ */
+goog.iter.starMap = function(iterable, f, opt_obj) {
+  var iterator = goog.iter.toIterator(iterable);
+  var iter = new goog.iter.Iterator();
+
+  iter.next = function() {
+    var args = goog.iter.toArray(iterator.next());
+    return f.apply(opt_obj, goog.array.concat(args, undefined, iterator));
+  };
+
+  return iter;
+};
+
+
+/**
+ * Returns an array of iterators each of which can iterate over the values in
+ * {@code iterable} without advancing the others.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.tee
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to tee.
+ * @param {number=} opt_num  The number of iterators to create. Default is 2.
+ * @return {!Array<goog.iter.Iterator<VALUE>>} An array of iterators.
+ * @template VALUE
+ */
+goog.iter.tee = function(iterable, opt_num) {
+  var iterator = goog.iter.toIterator(iterable);
+  var num = goog.isNumber(opt_num) ? opt_num : 2;
+  var buffers =
+      goog.array.map(goog.array.range(num), function() { return []; });
+
+  var addNextIteratorValueToBuffers = function() {
+    var val = iterator.next();
+    goog.array.forEach(buffers, function(buffer) { buffer.push(val); });
+  };
+
+  var createIterator = function(buffer) {
+    // Each tee'd iterator has an associated buffer (initially empty). When a
+    // tee'd iterator's buffer is empty, it calls
+    // addNextIteratorValueToBuffers(), adding the next value to all tee'd
+    // iterators' buffers, and then returns that value. This allows each
+    // iterator to be advanced independently.
+    var iter = new goog.iter.Iterator();
+
+    iter.next = function() {
+      if (goog.array.isEmpty(buffer)) {
+        addNextIteratorValueToBuffers();
+      }
+      goog.asserts.assert(!goog.array.isEmpty(buffer));
+      return buffer.shift();
+    };
+
+    return iter;
+  };
+
+  return goog.array.map(buffers, createIterator);
+};
+
+
+/**
+ * Creates an iterator that returns arrays containing a count and an element
+ * obtained from the given {@code iterable}.
+ * @see http://docs.python.org/2/library/functions.html#enumerate
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to enumerate.
+ * @param {number=} opt_start  Optional starting value. Default is 0.
+ * @return {!goog.iter.Iterator<!Array<?>>} A new iterator containing
+ *     count/item pairs.
+ * @template VALUE
+ */
+goog.iter.enumerate = function(iterable, opt_start) {
+  return goog.iter.zip(goog.iter.count(opt_start), iterable);
+};
+
+
+/**
+ * Creates an iterator that returns the first {@code limitSize} elements from an
+ * iterable. If this number is greater than the number of elements in the
+ * iterable, all the elements are returned.
+ * @see http://goo.gl/V0sihp Inspired by the limit iterator in Guava.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to limit.
+ * @param {number} limitSize  The maximum number of elements to return.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator containing
+ *     {@code limitSize} elements.
+ * @template VALUE
+ */
+goog.iter.limit = function(iterable, limitSize) {
+  goog.asserts.assert(goog.math.isInt(limitSize) && limitSize >= 0);
+
+  var iterator = goog.iter.toIterator(iterable);
+
+  var iter = new goog.iter.Iterator();
+  var remaining = limitSize;
+
+  iter.next = function() {
+    if (remaining-- > 0) {
+      return iterator.next();
+    }
+    throw goog.iter.StopIteration;
+  };
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that is advanced {@code count} steps ahead. Consumed
+ * values are silently discarded. If {@code count} is greater than the number
+ * of elements in {@code iterable}, an empty iterator is returned. Subsequent
+ * calls to {@code next()} will throw {@code goog.iter.StopIteration}.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to consume.
+ * @param {number} count  The number of elements to consume from the iterator.
+ * @return {!goog.iter.Iterator<VALUE>} An iterator advanced zero or more steps
+ *     ahead.
+ * @template VALUE
+ */
+goog.iter.consume = function(iterable, count) {
+  goog.asserts.assert(goog.math.isInt(count) && count >= 0);
+
+  var iterator = goog.iter.toIterator(iterable);
+
+  while (count-- > 0) {
+    goog.iter.nextOrValue(iterator, null);
+  }
+
+  return iterator;
+};
+
+
+/**
+ * Creates an iterator that returns a range of elements from an iterable.
+ * Similar to {@see goog.array#slice} but does not support negative indexes.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to slice.
+ * @param {number} start  The index of the first element to return.
+ * @param {number=} opt_end  The index after the last element to return. If
+ *     defined, must be greater than or equal to {@code start}.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator containing a slice of
+ *     the original.
+ * @template VALUE
+ */
+goog.iter.slice = function(iterable, start, opt_end) {
+  goog.asserts.assert(goog.math.isInt(start) && start >= 0);
+
+  var iterator = goog.iter.consume(iterable, start);
+
+  if (goog.isNumber(opt_end)) {
+    goog.asserts.assert(goog.math.isInt(opt_end) && opt_end >= start);
+    iterator = goog.iter.limit(iterator, opt_end - start /* limitSize */);
+  }
+
+  return iterator;
+};
+
+
+/**
+ * Checks an array for duplicate elements.
+ * @param {?IArrayLike<VALUE>} arr The array to check for
+ *     duplicates.
+ * @return {boolean} True, if the array contains duplicates, false otherwise.
+ * @private
+ * @template VALUE
+ */
+// TODO(dlindquist): Consider moving this into goog.array as a public function.
+goog.iter.hasDuplicates_ = function(arr) {
+  var deduped = [];
+  goog.array.removeDuplicates(arr, deduped);
+  return arr.length != deduped.length;
+};
+
+
+/**
+ * Creates an iterator that returns permutations of elements in
+ * {@code iterable}.
+ *
+ * Permutations are obtained by taking the Cartesian product of
+ * {@code opt_length} iterables and filtering out those with repeated
+ * elements. For example, the permutations of {@code [1,2,3]} are
+ * {@code [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]}.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.permutations
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable from which to generate permutations.
+ * @param {number=} opt_length Length of each permutation. If omitted, defaults
+ *     to the length of {@code iterable}.
+ * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator containing the
+ *     permutations of {@code iterable}.
+ * @template VALUE
+ */
+goog.iter.permutations = function(iterable, opt_length) {
+  var elements = goog.iter.toArray(iterable);
+  var length = goog.isNumber(opt_length) ? opt_length : elements.length;
+
+  var sets = goog.array.repeat(elements, length);
+  var product = goog.iter.product.apply(undefined, sets);
+
+  return goog.iter.filter(
+      product, function(arr) { return !goog.iter.hasDuplicates_(arr); });
+};
+
+
+/**
+ * Creates an iterator that returns combinations of elements from
+ * {@code iterable}.
+ *
+ * Combinations are obtained by taking the {@see goog.iter#permutations} of
+ * {@code iterable} and filtering those whose elements appear in the order they
+ * are encountered in {@code iterable}. For example, the 3-length combinations
+ * of {@code [0,1,2,3]} are {@code [[0,1,2], [0,1,3], [0,2,3], [1,2,3]]}.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.combinations
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable from which to generate combinations.
+ * @param {number} length The length of each combination.
+ * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator containing
+ *     combinations from the {@code iterable}.
+ * @template VALUE
+ */
+goog.iter.combinations = function(iterable, length) {
+  var elements = goog.iter.toArray(iterable);
+  var indexes = goog.iter.range(elements.length);
+  var indexIterator = goog.iter.permutations(indexes, length);
+  // sortedIndexIterator will now give arrays of with the given length that
+  // indicate what indexes into "elements" should be returned on each iteration.
+  var sortedIndexIterator = goog.iter.filter(
+      indexIterator, function(arr) { return goog.array.isSorted(arr); });
+
+  var iter = new goog.iter.Iterator();
+
+  function getIndexFromElements(index) { return elements[index]; }
+
+  iter.next = function() {
+    return goog.array.map(sortedIndexIterator.next(), getIndexFromElements);
+  };
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that returns combinations of elements from
+ * {@code iterable}, with repeated elements possible.
+ *
+ * Combinations are obtained by taking the Cartesian product of {@code length}
+ * iterables and filtering those whose elements appear in the order they are
+ * encountered in {@code iterable}. For example, the 2-length combinations of
+ * {@code [1,2,3]} are {@code [[1,1], [1,2], [1,3], [2,2], [2,3], [3,3]]}.
+ * @see https://goo.gl/C0yXe4
+ * @see https://goo.gl/djOCsk
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to combine.
+ * @param {number} length The length of each combination.
+ * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator containing
+ *     combinations from the {@code iterable}.
+ * @template VALUE
+ */
+goog.iter.combinationsWithReplacement = function(iterable, length) {
+  var elements = goog.iter.toArray(iterable);
+  var indexes = goog.array.range(elements.length);
+  var sets = goog.array.repeat(indexes, length);
+  var indexIterator = goog.iter.product.apply(undefined, sets);
+  // sortedIndexIterator will now give arrays of with the given length that
+  // indicate what indexes into "elements" should be returned on each iteration.
+  var sortedIndexIterator = goog.iter.filter(
+      indexIterator, function(arr) { return goog.array.isSorted(arr); });
+
+  var iter = new goog.iter.Iterator();
+
+  function getIndexFromElements(index) { return elements[index]; }
+
+  iter.next = function() {
+    return goog.array.map(
+        /** @type {!Array<number>} */
+        (sortedIndexIterator.next()), getIndexFromElements);
+  };
+
+  return iter;
+};
diff --git a/third_party/ink/closure/labs/useragent/browser.js b/third_party/ink/closure/labs/useragent/browser.js
new file mode 100644
index 0000000..78578e4
--- /dev/null
+++ b/third_party/ink/closure/labs/useragent/browser.js
@@ -0,0 +1,339 @@
+// Copyright 2013 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 Closure user agent detection (Browser).
+ * @see <a href="http://www.useragentstring.com/">User agent strings</a>
+ * For more information on rendering engine, platform, or device see the other
+ * sub-namespaces in goog.labs.userAgent, goog.labs.userAgent.platform,
+ * goog.labs.userAgent.device respectively.)
+ *
+ * @author vbhasin@google.com (Vipul Bhasin)
+ * @author martone@google.com (Andy Martone)
+ */
+
+goog.provide('goog.labs.userAgent.browser');
+
+goog.require('goog.array');
+goog.require('goog.labs.userAgent.util');
+goog.require('goog.object');
+goog.require('goog.string');
+
+
+// TODO(nnaze): Refactor to remove excessive exclusion logic in matching
+// functions.
+
+
+/**
+ * @return {boolean} Whether the user's browser is Opera.  Note: Chromium
+ *     based Opera (Opera 15+) is detected as Chrome to avoid unnecessary
+ *     special casing.
+ * @private
+ */
+goog.labs.userAgent.browser.matchOpera_ = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Opera');
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is IE.
+ * @private
+ */
+goog.labs.userAgent.browser.matchIE_ = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Trident') ||
+      goog.labs.userAgent.util.matchUserAgent('MSIE');
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is Edge.
+ * @private
+ */
+goog.labs.userAgent.browser.matchEdge_ = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Edge');
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is Firefox.
+ * @private
+ */
+goog.labs.userAgent.browser.matchFirefox_ = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Firefox');
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is Safari.
+ * @private
+ */
+goog.labs.userAgent.browser.matchSafari_ = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Safari') &&
+      !(goog.labs.userAgent.browser.matchChrome_() ||
+        goog.labs.userAgent.browser.matchCoast_() ||
+        goog.labs.userAgent.browser.matchOpera_() ||
+        goog.labs.userAgent.browser.matchEdge_() ||
+        goog.labs.userAgent.browser.isSilk() ||
+        goog.labs.userAgent.util.matchUserAgent('Android'));
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is Coast (Opera's Webkit-based
+ *     iOS browser).
+ * @private
+ */
+goog.labs.userAgent.browser.matchCoast_ = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Coast');
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is iOS Webview.
+ * @private
+ */
+goog.labs.userAgent.browser.matchIosWebview_ = function() {
+  // iOS Webview does not show up as Chrome or Safari. Also check for Opera's
+  // WebKit-based iOS browser, Coast.
+  return (goog.labs.userAgent.util.matchUserAgent('iPad') ||
+          goog.labs.userAgent.util.matchUserAgent('iPhone')) &&
+      !goog.labs.userAgent.browser.matchSafari_() &&
+      !goog.labs.userAgent.browser.matchChrome_() &&
+      !goog.labs.userAgent.browser.matchCoast_() &&
+      goog.labs.userAgent.util.matchUserAgent('AppleWebKit');
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is Chrome.
+ * @private
+ */
+goog.labs.userAgent.browser.matchChrome_ = function() {
+  return (goog.labs.userAgent.util.matchUserAgent('Chrome') ||
+          goog.labs.userAgent.util.matchUserAgent('CriOS')) &&
+      !goog.labs.userAgent.browser.matchEdge_();
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is the Android browser.
+ * @private
+ */
+goog.labs.userAgent.browser.matchAndroidBrowser_ = function() {
+  // Android can appear in the user agent string for Chrome on Android.
+  // This is not the Android standalone browser if it does.
+  return goog.labs.userAgent.util.matchUserAgent('Android') &&
+      !(goog.labs.userAgent.browser.isChrome() ||
+        goog.labs.userAgent.browser.isFirefox() ||
+        goog.labs.userAgent.browser.isOpera() ||
+        goog.labs.userAgent.browser.isSilk());
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is Opera.
+ */
+goog.labs.userAgent.browser.isOpera = goog.labs.userAgent.browser.matchOpera_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is IE.
+ */
+goog.labs.userAgent.browser.isIE = goog.labs.userAgent.browser.matchIE_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is Edge.
+ */
+goog.labs.userAgent.browser.isEdge = goog.labs.userAgent.browser.matchEdge_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is Firefox.
+ */
+goog.labs.userAgent.browser.isFirefox =
+    goog.labs.userAgent.browser.matchFirefox_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is Safari.
+ */
+goog.labs.userAgent.browser.isSafari = goog.labs.userAgent.browser.matchSafari_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is Coast (Opera's Webkit-based
+ *     iOS browser).
+ */
+goog.labs.userAgent.browser.isCoast = goog.labs.userAgent.browser.matchCoast_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is iOS Webview.
+ */
+goog.labs.userAgent.browser.isIosWebview =
+    goog.labs.userAgent.browser.matchIosWebview_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is Chrome.
+ */
+goog.labs.userAgent.browser.isChrome = goog.labs.userAgent.browser.matchChrome_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is the Android browser.
+ */
+goog.labs.userAgent.browser.isAndroidBrowser =
+    goog.labs.userAgent.browser.matchAndroidBrowser_;
+
+
+/**
+ * For more information, see:
+ * http://docs.aws.amazon.com/silk/latest/developerguide/user-agent.html
+ * @return {boolean} Whether the user's browser is Silk.
+ */
+goog.labs.userAgent.browser.isSilk = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Silk');
+};
+
+
+/**
+ * @return {string} The browser version or empty string if version cannot be
+ *     determined. Note that for Internet Explorer, this returns the version of
+ *     the browser, not the version of the rendering engine. (IE 8 in
+ *     compatibility mode will return 8.0 rather than 7.0. To determine the
+ *     rendering engine version, look at document.documentMode instead. See
+ *     http://msdn.microsoft.com/en-us/library/cc196988(v=vs.85).aspx for more
+ *     details.)
+ */
+goog.labs.userAgent.browser.getVersion = function() {
+  var userAgentString = goog.labs.userAgent.util.getUserAgent();
+  // Special case IE since IE's version is inside the parenthesis and
+  // without the '/'.
+  if (goog.labs.userAgent.browser.isIE()) {
+    return goog.labs.userAgent.browser.getIEVersion_(userAgentString);
+  }
+
+  var versionTuples =
+      goog.labs.userAgent.util.extractVersionTuples(userAgentString);
+
+  // Construct a map for easy lookup.
+  var versionMap = {};
+  goog.array.forEach(versionTuples, function(tuple) {
+    // Note that the tuple is of length three, but we only care about the
+    // first two.
+    var key = tuple[0];
+    var value = tuple[1];
+    versionMap[key] = value;
+  });
+
+  var versionMapHasKey = goog.partial(goog.object.containsKey, versionMap);
+
+  // Gives the value with the first key it finds, otherwise empty string.
+  function lookUpValueWithKeys(keys) {
+    var key = goog.array.find(keys, versionMapHasKey);
+    return versionMap[key] || '';
+  }
+
+  // Check Opera before Chrome since Opera 15+ has "Chrome" in the string.
+  // See
+  // http://my.opera.com/ODIN/blog/2013/07/15/opera-user-agent-strings-opera-15-and-beyond
+  if (goog.labs.userAgent.browser.isOpera()) {
+    // Opera 10 has Version/10.0 but Opera/9.8, so look for "Version" first.
+    // Opera uses 'OPR' for more recent UAs.
+    return lookUpValueWithKeys(['Version', 'Opera']);
+  }
+
+  // Check Edge before Chrome since it has Chrome in the string.
+  if (goog.labs.userAgent.browser.isEdge()) {
+    return lookUpValueWithKeys(['Edge']);
+  }
+
+  if (goog.labs.userAgent.browser.isChrome()) {
+    return lookUpValueWithKeys(['Chrome', 'CriOS']);
+  }
+
+  // Usually products browser versions are in the third tuple after "Mozilla"
+  // and the engine.
+  var tuple = versionTuples[2];
+  return tuple && tuple[1] || '';
+};
+
+
+/**
+ * @param {string|number} version The version to check.
+ * @return {boolean} Whether the browser version is higher or the same as the
+ *     given version.
+ */
+goog.labs.userAgent.browser.isVersionOrHigher = function(version) {
+  return goog.string.compareVersions(
+             goog.labs.userAgent.browser.getVersion(), version) >= 0;
+};
+
+
+/**
+ * Determines IE version. More information:
+ * http://msdn.microsoft.com/en-us/library/ie/bg182625(v=vs.85).aspx#uaString
+ * http://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx
+ * http://blogs.msdn.com/b/ie/archive/2010/03/23/introducing-ie9-s-user-agent-string.aspx
+ * http://blogs.msdn.com/b/ie/archive/2009/01/09/the-internet-explorer-8-user-agent-string-updated-edition.aspx
+ *
+ * @param {string} userAgent the User-Agent.
+ * @return {string}
+ * @private
+ */
+goog.labs.userAgent.browser.getIEVersion_ = function(userAgent) {
+  // IE11 may identify itself as MSIE 9.0 or MSIE 10.0 due to an IE 11 upgrade
+  // bug. Example UA:
+  // Mozilla/5.0 (MSIE 9.0; Windows NT 6.1; WOW64; Trident/7.0; rv:11.0)
+  // like Gecko.
+  // See http://www.whatismybrowser.com/developers/unknown-user-agent-fragments.
+  var rv = /rv: *([\d\.]*)/.exec(userAgent);
+  if (rv && rv[1]) {
+    return rv[1];
+  }
+
+  var version = '';
+  var msie = /MSIE +([\d\.]+)/.exec(userAgent);
+  if (msie && msie[1]) {
+    // IE in compatibility mode usually identifies itself as MSIE 7.0; in this
+    // case, use the Trident version to determine the version of IE. For more
+    // details, see the links above.
+    var tridentVersion = /Trident\/(\d.\d)/.exec(userAgent);
+    if (msie[1] == '7.0') {
+      if (tridentVersion && tridentVersion[1]) {
+        switch (tridentVersion[1]) {
+          case '4.0':
+            version = '8.0';
+            break;
+          case '5.0':
+            version = '9.0';
+            break;
+          case '6.0':
+            version = '10.0';
+            break;
+          case '7.0':
+            version = '11.0';
+            break;
+        }
+      } else {
+        version = '7.0';
+      }
+    } else {
+      version = msie[1];
+    }
+  }
+  return version;
+};
diff --git a/third_party/ink/closure/labs/useragent/engine.js b/third_party/ink/closure/labs/useragent/engine.js
new file mode 100644
index 0000000..4de0ff33
--- /dev/null
+++ b/third_party/ink/closure/labs/useragent/engine.js
@@ -0,0 +1,157 @@
+// Copyright 2013 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 Closure user agent detection.
+ * @see http://en.wikipedia.org/wiki/User_agent
+ * For more information on browser brand, platform, or device see the other
+ * sub-namespaces in goog.labs.userAgent (browser, platform, and device).
+ *
+ * @author vbhasin@google.com (Vipul Bhasin)
+ */
+
+goog.provide('goog.labs.userAgent.engine');
+
+goog.require('goog.array');
+goog.require('goog.labs.userAgent.util');
+goog.require('goog.string');
+
+
+/**
+ * @return {boolean} Whether the rendering engine is Presto.
+ */
+goog.labs.userAgent.engine.isPresto = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Presto');
+};
+
+
+/**
+ * @return {boolean} Whether the rendering engine is Trident.
+ */
+goog.labs.userAgent.engine.isTrident = function() {
+  // IE only started including the Trident token in IE8.
+  return goog.labs.userAgent.util.matchUserAgent('Trident') ||
+      goog.labs.userAgent.util.matchUserAgent('MSIE');
+};
+
+
+/**
+ * @return {boolean} Whether the rendering engine is Edge.
+ */
+goog.labs.userAgent.engine.isEdge = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Edge');
+};
+
+
+/**
+ * @return {boolean} Whether the rendering engine is WebKit.
+ */
+goog.labs.userAgent.engine.isWebKit = function() {
+  return goog.labs.userAgent.util.matchUserAgentIgnoreCase('WebKit') &&
+      !goog.labs.userAgent.engine.isEdge();
+};
+
+
+/**
+ * @return {boolean} Whether the rendering engine is Gecko.
+ */
+goog.labs.userAgent.engine.isGecko = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Gecko') &&
+      !goog.labs.userAgent.engine.isWebKit() &&
+      !goog.labs.userAgent.engine.isTrident() &&
+      !goog.labs.userAgent.engine.isEdge();
+};
+
+
+/**
+ * @return {string} The rendering engine's version or empty string if version
+ *     can't be determined.
+ */
+goog.labs.userAgent.engine.getVersion = function() {
+  var userAgentString = goog.labs.userAgent.util.getUserAgent();
+  if (userAgentString) {
+    var tuples = goog.labs.userAgent.util.extractVersionTuples(userAgentString);
+
+    var engineTuple = goog.labs.userAgent.engine.getEngineTuple_(tuples);
+    if (engineTuple) {
+      // In Gecko, the version string is either in the browser info or the
+      // Firefox version.  See Gecko user agent string reference:
+      // http://goo.gl/mULqa
+      if (engineTuple[0] == 'Gecko') {
+        return goog.labs.userAgent.engine.getVersionForKey_(tuples, 'Firefox');
+      }
+
+      return engineTuple[1];
+    }
+
+    // MSIE has only one version identifier, and the Trident version is
+    // specified in the parenthetical. IE Edge is covered in the engine tuple
+    // detection.
+    var browserTuple = tuples[0];
+    var info;
+    if (browserTuple && (info = browserTuple[2])) {
+      var match = /Trident\/([^\s;]+)/.exec(info);
+      if (match) {
+        return match[1];
+      }
+    }
+  }
+  return '';
+};
+
+
+/**
+ * @param {!Array<!Array<string>>} tuples Extracted version tuples.
+ * @return {!Array<string>|undefined} The engine tuple or undefined if not
+ *     found.
+ * @private
+ */
+goog.labs.userAgent.engine.getEngineTuple_ = function(tuples) {
+  if (!goog.labs.userAgent.engine.isEdge()) {
+    return tuples[1];
+  }
+  for (var i = 0; i < tuples.length; i++) {
+    var tuple = tuples[i];
+    if (tuple[0] == 'Edge') {
+      return tuple;
+    }
+  }
+};
+
+
+/**
+ * @param {string|number} version The version to check.
+ * @return {boolean} Whether the rendering engine version is higher or the same
+ *     as the given version.
+ */
+goog.labs.userAgent.engine.isVersionOrHigher = function(version) {
+  return goog.string.compareVersions(
+             goog.labs.userAgent.engine.getVersion(), version) >= 0;
+};
+
+
+/**
+ * @param {!Array<!Array<string>>} tuples Version tuples.
+ * @param {string} key The key to look for.
+ * @return {string} The version string of the given key, if present.
+ *     Otherwise, the empty string.
+ * @private
+ */
+goog.labs.userAgent.engine.getVersionForKey_ = function(tuples, key) {
+  // TODO(nnaze): Move to util if useful elsewhere.
+
+  var pair = goog.array.find(tuples, function(pair) { return key == pair[0]; });
+
+  return pair && pair[1] || '';
+};
diff --git a/third_party/ink/closure/labs/useragent/platform.js b/third_party/ink/closure/labs/useragent/platform.js
new file mode 100644
index 0000000..d5c3537
--- /dev/null
+++ b/third_party/ink/closure/labs/useragent/platform.js
@@ -0,0 +1,161 @@
+// Copyright 2013 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 Closure user agent platform detection.
+ * @see <a href="http://www.useragentstring.com/">User agent strings</a>
+ * For more information on browser brand, rendering engine, or device see the
+ * other sub-namespaces in goog.labs.userAgent (browser, engine, and device
+ * respectively).
+ *
+ * @author vbhasin@google.com (Vipul Bhasin)
+ */
+
+goog.provide('goog.labs.userAgent.platform');
+
+goog.require('goog.labs.userAgent.util');
+goog.require('goog.string');
+
+
+/**
+ * @return {boolean} Whether the platform is Android.
+ */
+goog.labs.userAgent.platform.isAndroid = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Android');
+};
+
+
+/**
+ * @return {boolean} Whether the platform is iPod.
+ */
+goog.labs.userAgent.platform.isIpod = function() {
+  return goog.labs.userAgent.util.matchUserAgent('iPod');
+};
+
+
+/**
+ * @return {boolean} Whether the platform is iPhone.
+ */
+goog.labs.userAgent.platform.isIphone = function() {
+  return goog.labs.userAgent.util.matchUserAgent('iPhone') &&
+      !goog.labs.userAgent.util.matchUserAgent('iPod') &&
+      !goog.labs.userAgent.util.matchUserAgent('iPad');
+};
+
+
+/**
+ * @return {boolean} Whether the platform is iPad.
+ */
+goog.labs.userAgent.platform.isIpad = function() {
+  return goog.labs.userAgent.util.matchUserAgent('iPad');
+};
+
+
+/**
+ * @return {boolean} Whether the platform is iOS.
+ */
+goog.labs.userAgent.platform.isIos = function() {
+  return goog.labs.userAgent.platform.isIphone() ||
+      goog.labs.userAgent.platform.isIpad() ||
+      goog.labs.userAgent.platform.isIpod();
+};
+
+
+/**
+ * @return {boolean} Whether the platform is Mac.
+ */
+goog.labs.userAgent.platform.isMacintosh = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Macintosh');
+};
+
+
+/**
+ * Note: ChromeOS is not considered to be Linux as it does not report itself
+ * as Linux in the user agent string.
+ * @return {boolean} Whether the platform is Linux.
+ */
+goog.labs.userAgent.platform.isLinux = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Linux');
+};
+
+
+/**
+ * @return {boolean} Whether the platform is Windows.
+ */
+goog.labs.userAgent.platform.isWindows = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Windows');
+};
+
+
+/**
+ * @return {boolean} Whether the platform is ChromeOS.
+ */
+goog.labs.userAgent.platform.isChromeOS = function() {
+  return goog.labs.userAgent.util.matchUserAgent('CrOS');
+};
+
+
+/**
+ * The version of the platform. We only determine the version for Windows,
+ * Mac, and Chrome OS. It doesn't make much sense on Linux. For Windows, we only
+ * look at the NT version. Non-NT-based versions (e.g. 95, 98, etc.) are given
+ * version 0.0.
+ *
+ * @return {string} The platform version or empty string if version cannot be
+ *     determined.
+ */
+goog.labs.userAgent.platform.getVersion = function() {
+  var userAgentString = goog.labs.userAgent.util.getUserAgent();
+  var version = '', re;
+  if (goog.labs.userAgent.platform.isWindows()) {
+    re = /Windows (?:NT|Phone) ([0-9.]+)/;
+    var match = re.exec(userAgentString);
+    if (match) {
+      version = match[1];
+    } else {
+      version = '0.0';
+    }
+  } else if (goog.labs.userAgent.platform.isIos()) {
+    re = /(?:iPhone|iPod|iPad|CPU)\s+OS\s+(\S+)/;
+    var match = re.exec(userAgentString);
+    // Report the version as x.y.z and not x_y_z
+    version = match && match[1].replace(/_/g, '.');
+  } else if (goog.labs.userAgent.platform.isMacintosh()) {
+    re = /Mac OS X ([0-9_.]+)/;
+    var match = re.exec(userAgentString);
+    // Note: some old versions of Camino do not report an OSX version.
+    // Default to 10.
+    version = match ? match[1].replace(/_/g, '.') : '10';
+  } else if (goog.labs.userAgent.platform.isAndroid()) {
+    re = /Android\s+([^\);]+)(\)|;)/;
+    var match = re.exec(userAgentString);
+    version = match && match[1];
+  } else if (goog.labs.userAgent.platform.isChromeOS()) {
+    re = /(?:CrOS\s+(?:i686|x86_64)\s+([0-9.]+))/;
+    var match = re.exec(userAgentString);
+    version = match && match[1];
+  }
+  return version || '';
+};
+
+
+/**
+ * @param {string|number} version The version to check.
+ * @return {boolean} Whether the browser version is higher or the same as the
+ *     given version.
+ */
+goog.labs.userAgent.platform.isVersionOrHigher = function(version) {
+  return goog.string.compareVersions(
+             goog.labs.userAgent.platform.getVersion(), version) >= 0;
+};
diff --git a/third_party/ink/closure/labs/useragent/util.js b/third_party/ink/closure/labs/useragent/util.js
new file mode 100644
index 0000000..a57e5d8d
--- /dev/null
+++ b/third_party/ink/closure/labs/useragent/util.js
@@ -0,0 +1,157 @@
+// Copyright 2013 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 Utilities used by goog.labs.userAgent tools. These functions
+ * should not be used outside of goog.labs.userAgent.*.
+ *
+ * MOE:begin_intracomment_strip
+ * @visibility {//javascript/abc/libs/objects3d:__subpackages__}
+ * @visibility {//javascript/closure/bin/sizetests:__pkg__}
+ * @visibility {//javascript/closure/dom:__subpackages__}
+ * @visibility {//javascript/closure/style:__pkg__}
+ * @visibility {//javascript/closure/testing:__pkg__}
+ * @visibility {//javascript/closure/useragent:__subpackages__}
+ * @visibility {//testing/puppet/modules:__pkg__}
+ * @visibility {:util_legacy_users}
+ * MOE:end_intracomment_strip
+ *
+ * @author nnaze@google.com (Nathan Naze)
+ */
+
+goog.provide('goog.labs.userAgent.util');
+
+goog.require('goog.string');
+
+
+/**
+ * Gets the native userAgent string from navigator if it exists.
+ * If navigator or navigator.userAgent string is missing, returns an empty
+ * string.
+ * @return {string}
+ * @private
+ */
+goog.labs.userAgent.util.getNativeUserAgentString_ = function() {
+  var navigator = goog.labs.userAgent.util.getNavigator_();
+  if (navigator) {
+    var userAgent = navigator.userAgent;
+    if (userAgent) {
+      return userAgent;
+    }
+  }
+  return '';
+};
+
+
+/**
+ * Getter for the native navigator.
+ * This is a separate function so it can be stubbed out in testing.
+ * @return {Navigator}
+ * @private
+ */
+goog.labs.userAgent.util.getNavigator_ = function() {
+  return goog.global.navigator;
+};
+
+
+/**
+ * A possible override for applications which wish to not check
+ * navigator.userAgent but use a specified value for detection instead.
+ * @private {string}
+ */
+goog.labs.userAgent.util.userAgent_ =
+    goog.labs.userAgent.util.getNativeUserAgentString_();
+
+
+/**
+ * Applications may override browser detection on the built in
+ * navigator.userAgent object by setting this string. Set to null to use the
+ * browser object instead.
+ * @param {?string=} opt_userAgent The User-Agent override.
+ */
+goog.labs.userAgent.util.setUserAgent = function(opt_userAgent) {
+  goog.labs.userAgent.util.userAgent_ =
+      opt_userAgent || goog.labs.userAgent.util.getNativeUserAgentString_();
+};
+
+
+/**
+ * @return {string} The user agent string.
+ */
+goog.labs.userAgent.util.getUserAgent = function() {
+  return goog.labs.userAgent.util.userAgent_;
+};
+
+
+/**
+ * @param {string} str
+ * @return {boolean} Whether the user agent contains the given string.
+ */
+goog.labs.userAgent.util.matchUserAgent = function(str) {
+  var userAgent = goog.labs.userAgent.util.getUserAgent();
+  return goog.string.contains(userAgent, str);
+};
+
+
+/**
+ * @param {string} str
+ * @return {boolean} Whether the user agent contains the given string, ignoring
+ *     case.
+ */
+goog.labs.userAgent.util.matchUserAgentIgnoreCase = function(str) {
+  var userAgent = goog.labs.userAgent.util.getUserAgent();
+  return goog.string.caseInsensitiveContains(userAgent, str);
+};
+
+
+/**
+ * Parses the user agent into tuples for each section.
+ * @param {string} userAgent
+ * @return {!Array<!Array<string>>} Tuples of key, version, and the contents
+ *     of the parenthetical.
+ */
+goog.labs.userAgent.util.extractVersionTuples = function(userAgent) {
+  // Matches each section of a user agent string.
+  // Example UA:
+  // Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OS X; en-us)
+  // AppleWebKit/531.21.10 (KHTML, like Gecko) Mobile/7B405
+  // This has three version tuples: Mozilla, AppleWebKit, and Mobile.
+
+  var versionRegExp = new RegExp(
+      // Key. Note that a key may have a space.
+      // (i.e. 'Mobile Safari' in 'Mobile Safari/5.0')
+      '(\\w[\\w ]+)' +
+
+          '/' +                // slash
+          '([^\\s]+)' +        // version (i.e. '5.0b')
+          '\\s*' +             // whitespace
+          '(?:\\((.*?)\\))?',  // parenthetical info. parentheses not matched.
+      'g');
+
+  var data = [];
+  var match;
+
+  // Iterate and collect the version tuples.  Each iteration will be the
+  // next regex match.
+  while (match = versionRegExp.exec(userAgent)) {
+    data.push([
+      match[1],  // key
+      match[2],  // value
+      // || undefined as this is not undefined in IE7 and IE8
+      match[3] || undefined  // info
+    ]);
+  }
+
+  return data;
+};
diff --git a/third_party/ink/closure/math/box.js b/third_party/ink/closure/math/box.js
new file mode 100644
index 0000000..108e6f7
--- /dev/null
+++ b/third_party/ink/closure/math/box.js
@@ -0,0 +1,403 @@
+// Copyright 2006 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 A utility class for representing a numeric box.
+ * @author pupius@google.com (Daniel Pupius)
+ */
+
+
+goog.provide('goog.math.Box');
+
+goog.require('goog.asserts');
+goog.require('goog.math.Coordinate');
+
+
+
+/**
+ * Class for representing a box. A box is specified as a top, right, bottom,
+ * and left. A box is useful for representing margins and padding.
+ *
+ * This class assumes 'screen coordinates': larger Y coordinates are further
+ * from the top of the screen.
+ *
+ * @param {number} top Top.
+ * @param {number} right Right.
+ * @param {number} bottom Bottom.
+ * @param {number} left Left.
+ * @struct
+ * @constructor
+ */
+goog.math.Box = function(top, right, bottom, left) {
+  /**
+   * Top
+   * @type {number}
+   */
+  this.top = top;
+
+  /**
+   * Right
+   * @type {number}
+   */
+  this.right = right;
+
+  /**
+   * Bottom
+   * @type {number}
+   */
+  this.bottom = bottom;
+
+  /**
+   * Left
+   * @type {number}
+   */
+  this.left = left;
+};
+
+
+/**
+ * Creates a Box by bounding a collection of goog.math.Coordinate objects
+ * @param {...goog.math.Coordinate} var_args Coordinates to be included inside
+ *     the box.
+ * @return {!goog.math.Box} A Box containing all the specified Coordinates.
+ */
+goog.math.Box.boundingBox = function(var_args) {
+  var box = new goog.math.Box(
+      arguments[0].y, arguments[0].x, arguments[0].y, arguments[0].x);
+  for (var i = 1; i < arguments.length; i++) {
+    box.expandToIncludeCoordinate(arguments[i]);
+  }
+  return box;
+};
+
+
+/**
+ * @return {number} width The width of this Box.
+ */
+goog.math.Box.prototype.getWidth = function() {
+  return this.right - this.left;
+};
+
+
+/**
+ * @return {number} height The height of this Box.
+ */
+goog.math.Box.prototype.getHeight = function() {
+  return this.bottom - this.top;
+};
+
+
+/**
+ * Creates a copy of the box with the same dimensions.
+ * @return {!goog.math.Box} A clone of this Box.
+ */
+goog.math.Box.prototype.clone = function() {
+  return new goog.math.Box(this.top, this.right, this.bottom, this.left);
+};
+
+
+if (goog.DEBUG) {
+  /**
+   * Returns a nice string representing the box.
+   * @return {string} In the form (50t, 73r, 24b, 13l).
+   * @override
+   */
+  goog.math.Box.prototype.toString = function() {
+    return '(' + this.top + 't, ' + this.right + 'r, ' + this.bottom + 'b, ' +
+        this.left + 'l)';
+  };
+}
+
+
+/**
+ * Returns whether the box contains a coordinate or another box.
+ *
+ * @param {goog.math.Coordinate|goog.math.Box} other A Coordinate or a Box.
+ * @return {boolean} Whether the box contains the coordinate or other box.
+ */
+goog.math.Box.prototype.contains = function(other) {
+  return goog.math.Box.contains(this, other);
+};
+
+
+/**
+ * Expands box with the given margins.
+ *
+ * @param {number|goog.math.Box} top Top margin or box with all margins.
+ * @param {number=} opt_right Right margin.
+ * @param {number=} opt_bottom Bottom margin.
+ * @param {number=} opt_left Left margin.
+ * @return {!goog.math.Box} A reference to this Box.
+ */
+goog.math.Box.prototype.expand = function(
+    top, opt_right, opt_bottom, opt_left) {
+  if (goog.isObject(top)) {
+    this.top -= top.top;
+    this.right += top.right;
+    this.bottom += top.bottom;
+    this.left -= top.left;
+  } else {
+    this.top -= /** @type {number} */ (top);
+    this.right += Number(opt_right);
+    this.bottom += Number(opt_bottom);
+    this.left -= Number(opt_left);
+  }
+
+  return this;
+};
+
+
+/**
+ * Expand this box to include another box.
+ * NOTE(bcornell): This is used in code that needs to be very fast, please don't
+ * add functionality to this function at the expense of speed (variable
+ * arguments, accepting multiple argument types, etc).
+ * @param {goog.math.Box} box The box to include in this one.
+ */
+goog.math.Box.prototype.expandToInclude = function(box) {
+  this.left = Math.min(this.left, box.left);
+  this.top = Math.min(this.top, box.top);
+  this.right = Math.max(this.right, box.right);
+  this.bottom = Math.max(this.bottom, box.bottom);
+};
+
+
+/**
+ * Expand this box to include the coordinate.
+ * @param {!goog.math.Coordinate} coord The coordinate to be included
+ *     inside the box.
+ */
+goog.math.Box.prototype.expandToIncludeCoordinate = function(coord) {
+  this.top = Math.min(this.top, coord.y);
+  this.right = Math.max(this.right, coord.x);
+  this.bottom = Math.max(this.bottom, coord.y);
+  this.left = Math.min(this.left, coord.x);
+};
+
+
+/**
+ * Compares boxes for equality.
+ * @param {goog.math.Box} a A Box.
+ * @param {goog.math.Box} b A Box.
+ * @return {boolean} True iff the boxes are equal, or if both are null.
+ */
+goog.math.Box.equals = function(a, b) {
+  if (a == b) {
+    return true;
+  }
+  if (!a || !b) {
+    return false;
+  }
+  return a.top == b.top && a.right == b.right && a.bottom == b.bottom &&
+      a.left == b.left;
+};
+
+
+/**
+ * Returns whether a box contains a coordinate or another box.
+ *
+ * @param {goog.math.Box} box A Box.
+ * @param {goog.math.Coordinate|goog.math.Box} other A Coordinate or a Box.
+ * @return {boolean} Whether the box contains the coordinate or other box.
+ */
+goog.math.Box.contains = function(box, other) {
+  if (!box || !other) {
+    return false;
+  }
+
+  if (other instanceof goog.math.Box) {
+    return other.left >= box.left && other.right <= box.right &&
+        other.top >= box.top && other.bottom <= box.bottom;
+  }
+
+  // other is a Coordinate.
+  return other.x >= box.left && other.x <= box.right && other.y >= box.top &&
+      other.y <= box.bottom;
+};
+
+
+/**
+ * Returns the relative x position of a coordinate compared to a box.  Returns
+ * zero if the coordinate is inside the box.
+ *
+ * @param {goog.math.Box} box A Box.
+ * @param {goog.math.Coordinate} coord A Coordinate.
+ * @return {number} The x position of {@code coord} relative to the nearest
+ *     side of {@code box}, or zero if {@code coord} is inside {@code box}.
+ */
+goog.math.Box.relativePositionX = function(box, coord) {
+  if (coord.x < box.left) {
+    return coord.x - box.left;
+  } else if (coord.x > box.right) {
+    return coord.x - box.right;
+  }
+  return 0;
+};
+
+
+/**
+ * Returns the relative y position of a coordinate compared to a box.  Returns
+ * zero if the coordinate is inside the box.
+ *
+ * @param {goog.math.Box} box A Box.
+ * @param {goog.math.Coordinate} coord A Coordinate.
+ * @return {number} The y position of {@code coord} relative to the nearest
+ *     side of {@code box}, or zero if {@code coord} is inside {@code box}.
+ */
+goog.math.Box.relativePositionY = function(box, coord) {
+  if (coord.y < box.top) {
+    return coord.y - box.top;
+  } else if (coord.y > box.bottom) {
+    return coord.y - box.bottom;
+  }
+  return 0;
+};
+
+
+/**
+ * Returns the distance between a coordinate and the nearest corner/side of a
+ * box. Returns zero if the coordinate is inside the box.
+ *
+ * @param {goog.math.Box} box A Box.
+ * @param {goog.math.Coordinate} coord A Coordinate.
+ * @return {number} The distance between {@code coord} and the nearest
+ *     corner/side of {@code box}, or zero if {@code coord} is inside
+ *     {@code box}.
+ */
+goog.math.Box.distance = function(box, coord) {
+  var x = goog.math.Box.relativePositionX(box, coord);
+  var y = goog.math.Box.relativePositionY(box, coord);
+  return Math.sqrt(x * x + y * y);
+};
+
+
+/**
+ * Returns whether two boxes intersect.
+ *
+ * @param {goog.math.Box} a A Box.
+ * @param {goog.math.Box} b A second Box.
+ * @return {boolean} Whether the boxes intersect.
+ */
+goog.math.Box.intersects = function(a, b) {
+  return (
+      a.left <= b.right && b.left <= a.right && a.top <= b.bottom &&
+      b.top <= a.bottom);
+};
+
+
+/**
+ * Returns whether two boxes would intersect with additional padding.
+ *
+ * @param {goog.math.Box} a A Box.
+ * @param {goog.math.Box} b A second Box.
+ * @param {number} padding The additional padding.
+ * @return {boolean} Whether the boxes intersect.
+ */
+goog.math.Box.intersectsWithPadding = function(a, b, padding) {
+  return (
+      a.left <= b.right + padding && b.left <= a.right + padding &&
+      a.top <= b.bottom + padding && b.top <= a.bottom + padding);
+};
+
+
+/**
+ * Rounds the fields to the next larger integer values.
+ *
+ * @return {!goog.math.Box} This box with ceil'd fields.
+ */
+goog.math.Box.prototype.ceil = function() {
+  this.top = Math.ceil(this.top);
+  this.right = Math.ceil(this.right);
+  this.bottom = Math.ceil(this.bottom);
+  this.left = Math.ceil(this.left);
+  return this;
+};
+
+
+/**
+ * Rounds the fields to the next smaller integer values.
+ *
+ * @return {!goog.math.Box} This box with floored fields.
+ */
+goog.math.Box.prototype.floor = function() {
+  this.top = Math.floor(this.top);
+  this.right = Math.floor(this.right);
+  this.bottom = Math.floor(this.bottom);
+  this.left = Math.floor(this.left);
+  return this;
+};
+
+
+/**
+ * Rounds the fields to nearest integer values.
+ *
+ * @return {!goog.math.Box} This box with rounded fields.
+ */
+goog.math.Box.prototype.round = function() {
+  this.top = Math.round(this.top);
+  this.right = Math.round(this.right);
+  this.bottom = Math.round(this.bottom);
+  this.left = Math.round(this.left);
+  return this;
+};
+
+
+/**
+ * Translates this box by the given offsets. If a {@code goog.math.Coordinate}
+ * is given, then the left and right values are translated by the coordinate's
+ * x value and the top and bottom values are translated by the coordinate's y
+ * value.  Otherwise, {@code tx} and {@code opt_ty} are used to translate the x
+ * and y dimension values.
+ *
+ * @param {number|goog.math.Coordinate} tx The value to translate the x
+ *     dimension values by or the the coordinate to translate this box by.
+ * @param {number=} opt_ty The value to translate y dimension values by.
+ * @return {!goog.math.Box} This box after translating.
+ */
+goog.math.Box.prototype.translate = function(tx, opt_ty) {
+  if (tx instanceof goog.math.Coordinate) {
+    this.left += tx.x;
+    this.right += tx.x;
+    this.top += tx.y;
+    this.bottom += tx.y;
+  } else {
+    goog.asserts.assertNumber(tx);
+    this.left += tx;
+    this.right += tx;
+    if (goog.isNumber(opt_ty)) {
+      this.top += opt_ty;
+      this.bottom += opt_ty;
+    }
+  }
+  return this;
+};
+
+
+/**
+ * Scales this coordinate by the given scale factors. The x and y dimension
+ * values are scaled by {@code sx} and {@code opt_sy} respectively.
+ * If {@code opt_sy} is not given, then {@code sx} is used for both x and y.
+ *
+ * @param {number} sx The scale factor to use for the x dimension.
+ * @param {number=} opt_sy The scale factor to use for the y dimension.
+ * @return {!goog.math.Box} This box after scaling.
+ */
+goog.math.Box.prototype.scale = function(sx, opt_sy) {
+  var sy = goog.isNumber(opt_sy) ? opt_sy : sx;
+  this.left *= sx;
+  this.right *= sx;
+  this.top *= sy;
+  this.bottom *= sy;
+  return this;
+};
diff --git a/third_party/ink/closure/math/coordinate.js b/third_party/ink/closure/math/coordinate.js
new file mode 100644
index 0000000..6966451d
--- /dev/null
+++ b/third_party/ink/closure/math/coordinate.js
@@ -0,0 +1,280 @@
+// Copyright 2006 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 A utility class for representing two-dimensional positions.
+ * @author pupius@google.com (Daniel Pupius)
+ */
+
+
+goog.provide('goog.math.Coordinate');
+
+goog.require('goog.math');
+
+
+
+/**
+ * Class for representing coordinates and positions.
+ * @param {number=} opt_x Left, defaults to 0.
+ * @param {number=} opt_y Top, defaults to 0.
+ * @struct
+ * @constructor
+ */
+goog.math.Coordinate = function(opt_x, opt_y) {
+  /**
+   * X-value
+   * @type {number}
+   */
+  this.x = goog.isDef(opt_x) ? opt_x : 0;
+
+  /**
+   * Y-value
+   * @type {number}
+   */
+  this.y = goog.isDef(opt_y) ? opt_y : 0;
+};
+
+
+/**
+ * Returns a new copy of the coordinate.
+ * @return {!goog.math.Coordinate} A clone of this coordinate.
+ */
+goog.math.Coordinate.prototype.clone = function() {
+  return new goog.math.Coordinate(this.x, this.y);
+};
+
+
+if (goog.DEBUG) {
+  /**
+   * Returns a nice string representing the coordinate.
+   * @return {string} In the form (50, 73).
+   * @override
+   */
+  goog.math.Coordinate.prototype.toString = function() {
+    return '(' + this.x + ', ' + this.y + ')';
+  };
+}
+
+
+/**
+ * Returns whether the specified value is equal to this coordinate.
+ * @param {*} other Some other value.
+ * @return {boolean} Whether the specified value is equal to this coordinate.
+ */
+goog.math.Coordinate.prototype.equals = function(other) {
+  return other instanceof goog.math.Coordinate &&
+      goog.math.Coordinate.equals(this, other);
+};
+
+
+/**
+ * Compares coordinates for equality.
+ * @param {goog.math.Coordinate} a A Coordinate.
+ * @param {goog.math.Coordinate} b A Coordinate.
+ * @return {boolean} True iff the coordinates are equal, or if both are null.
+ */
+goog.math.Coordinate.equals = function(a, b) {
+  if (a == b) {
+    return true;
+  }
+  if (!a || !b) {
+    return false;
+  }
+  return a.x == b.x && a.y == b.y;
+};
+
+
+/**
+ * Returns the distance between two coordinates.
+ * @param {!goog.math.Coordinate} a A Coordinate.
+ * @param {!goog.math.Coordinate} b A Coordinate.
+ * @return {number} The distance between {@code a} and {@code b}.
+ */
+goog.math.Coordinate.distance = function(a, b) {
+  var dx = a.x - b.x;
+  var dy = a.y - b.y;
+  return Math.sqrt(dx * dx + dy * dy);
+};
+
+
+/**
+ * Returns the magnitude of a coordinate.
+ * @param {!goog.math.Coordinate} a A Coordinate.
+ * @return {number} The distance between the origin and {@code a}.
+ */
+goog.math.Coordinate.magnitude = function(a) {
+  return Math.sqrt(a.x * a.x + a.y * a.y);
+};
+
+
+/**
+ * Returns the angle from the origin to a coordinate.
+ * @param {!goog.math.Coordinate} a A Coordinate.
+ * @return {number} The angle, in degrees, clockwise from the positive X
+ *     axis to {@code a}.
+ */
+goog.math.Coordinate.azimuth = function(a) {
+  return goog.math.angle(0, 0, a.x, a.y);
+};
+
+
+/**
+ * Returns the squared distance between two coordinates. Squared distances can
+ * be used for comparisons when the actual value is not required.
+ *
+ * Performance note: eliminating the square root is an optimization often used
+ * in lower-level languages, but the speed difference is not nearly as
+ * pronounced in JavaScript (only a few percent.)
+ *
+ * @param {!goog.math.Coordinate} a A Coordinate.
+ * @param {!goog.math.Coordinate} b A Coordinate.
+ * @return {number} The squared distance between {@code a} and {@code b}.
+ */
+goog.math.Coordinate.squaredDistance = function(a, b) {
+  var dx = a.x - b.x;
+  var dy = a.y - b.y;
+  return dx * dx + dy * dy;
+};
+
+
+/**
+ * Returns the difference between two coordinates as a new
+ * goog.math.Coordinate.
+ * @param {!goog.math.Coordinate} a A Coordinate.
+ * @param {!goog.math.Coordinate} b A Coordinate.
+ * @return {!goog.math.Coordinate} A Coordinate representing the difference
+ *     between {@code a} and {@code b}.
+ */
+goog.math.Coordinate.difference = function(a, b) {
+  return new goog.math.Coordinate(a.x - b.x, a.y - b.y);
+};
+
+
+/**
+ * Returns the sum of two coordinates as a new goog.math.Coordinate.
+ * @param {!goog.math.Coordinate} a A Coordinate.
+ * @param {!goog.math.Coordinate} b A Coordinate.
+ * @return {!goog.math.Coordinate} A Coordinate representing the sum of the two
+ *     coordinates.
+ */
+goog.math.Coordinate.sum = function(a, b) {
+  return new goog.math.Coordinate(a.x + b.x, a.y + b.y);
+};
+
+
+/**
+ * Rounds the x and y fields to the next larger integer values.
+ * @return {!goog.math.Coordinate} This coordinate with ceil'd fields.
+ */
+goog.math.Coordinate.prototype.ceil = function() {
+  this.x = Math.ceil(this.x);
+  this.y = Math.ceil(this.y);
+  return this;
+};
+
+
+/**
+ * Rounds the x and y fields to the next smaller integer values.
+ * @return {!goog.math.Coordinate} This coordinate with floored fields.
+ */
+goog.math.Coordinate.prototype.floor = function() {
+  this.x = Math.floor(this.x);
+  this.y = Math.floor(this.y);
+  return this;
+};
+
+
+/**
+ * Rounds the x and y fields to the nearest integer values.
+ * @return {!goog.math.Coordinate} This coordinate with rounded fields.
+ */
+goog.math.Coordinate.prototype.round = function() {
+  this.x = Math.round(this.x);
+  this.y = Math.round(this.y);
+  return this;
+};
+
+
+/**
+ * Translates this box by the given offsets. If a {@code goog.math.Coordinate}
+ * is given, then the x and y values are translated by the coordinate's x and y.
+ * Otherwise, x and y are translated by {@code tx} and {@code opt_ty}
+ * respectively.
+ * @param {number|goog.math.Coordinate} tx The value to translate x by or the
+ *     the coordinate to translate this coordinate by.
+ * @param {number=} opt_ty The value to translate y by.
+ * @return {!goog.math.Coordinate} This coordinate after translating.
+ */
+goog.math.Coordinate.prototype.translate = function(tx, opt_ty) {
+  if (tx instanceof goog.math.Coordinate) {
+    this.x += tx.x;
+    this.y += tx.y;
+  } else {
+    this.x += Number(tx);
+    if (goog.isNumber(opt_ty)) {
+      this.y += opt_ty;
+    }
+  }
+  return this;
+};
+
+
+/**
+ * Scales this coordinate by the given scale factors. The x and y values are
+ * scaled by {@code sx} and {@code opt_sy} respectively.  If {@code opt_sy}
+ * is not given, then {@code sx} is used for both x and y.
+ * @param {number} sx The scale factor to use for the x dimension.
+ * @param {number=} opt_sy The scale factor to use for the y dimension.
+ * @return {!goog.math.Coordinate} This coordinate after scaling.
+ */
+goog.math.Coordinate.prototype.scale = function(sx, opt_sy) {
+  var sy = goog.isNumber(opt_sy) ? opt_sy : sx;
+  this.x *= sx;
+  this.y *= sy;
+  return this;
+};
+
+
+/**
+ * Rotates this coordinate clockwise about the origin (or, optionally, the given
+ * center) by the given angle, in radians.
+ * @param {number} radians The angle by which to rotate this coordinate
+ *     clockwise about the given center, in radians.
+ * @param {!goog.math.Coordinate=} opt_center The center of rotation. Defaults
+ *     to (0, 0) if not given.
+ */
+goog.math.Coordinate.prototype.rotateRadians = function(radians, opt_center) {
+  var center = opt_center || new goog.math.Coordinate(0, 0);
+
+  var x = this.x;
+  var y = this.y;
+  var cos = Math.cos(radians);
+  var sin = Math.sin(radians);
+
+  this.x = (x - center.x) * cos - (y - center.y) * sin + center.x;
+  this.y = (x - center.x) * sin + (y - center.y) * cos + center.y;
+};
+
+
+/**
+ * Rotates this coordinate clockwise about the origin (or, optionally, the given
+ * center) by the given angle, in degrees.
+ * @param {number} degrees The angle by which to rotate this coordinate
+ *     clockwise about the given center, in degrees.
+ * @param {!goog.math.Coordinate=} opt_center The center of rotation. Defaults
+ *     to (0, 0) if not given.
+ */
+goog.math.Coordinate.prototype.rotateDegrees = function(degrees, opt_center) {
+  this.rotateRadians(goog.math.toRadians(degrees), opt_center);
+};
diff --git a/third_party/ink/closure/math/irect.js b/third_party/ink/closure/math/irect.js
new file mode 100644
index 0000000..db7cee1
--- /dev/null
+++ b/third_party/ink/closure/math/irect.js
@@ -0,0 +1,45 @@
+// Copyright 2016 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 A record declaration to allow ClientRect and other rectangle
+ * like objects to be used with goog.math.Rect.
+ */
+
+goog.provide('goog.math.IRect');
+
+
+/**
+ * Record for representing rectangular regions, allows compatibility between
+ * things like ClientRect and goog.math.Rect.
+ *
+ * @record
+ */
+goog.math.IRect = function() {};
+
+
+/** @type {number} */
+goog.math.IRect.prototype.left;
+
+
+/** @type {number} */
+goog.math.IRect.prototype.top;
+
+
+/** @type {number} */
+goog.math.IRect.prototype.width;
+
+
+/** @type {number} */
+goog.math.IRect.prototype.height;
diff --git a/third_party/ink/closure/math/long.js b/third_party/ink/closure/math/long.js
new file mode 100644
index 0000000..60ddef0
--- /dev/null
+++ b/third_party/ink/closure/math/long.js
@@ -0,0 +1,966 @@
+// Copyright 2009 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 Defines a Long class for representing a 64-bit two's-complement
+ * integer value, which faithfully simulates the behavior of a Java "long". This
+ * implementation is derived from LongLib in GWT.
+ *
+ * @author kevinz@google.com (Kevin Zatloukal)
+ */
+
+goog.provide('goog.math.Long');
+
+goog.require('goog.asserts');
+goog.require('goog.reflect');
+
+
+
+/**
+ * Constructs a 64-bit two's-complement integer, given its low and high 32-bit
+ * values as *signed* integers.  See the from* functions below for more
+ * convenient ways of constructing Longs.
+ *
+ * The internal representation of a long is the two given signed, 32-bit values.
+ * We use 32-bit pieces because these are the size of integers on which
+ * Javascript performs bit-operations.  For operations like addition and
+ * multiplication, we split each number into 16-bit pieces, which can easily be
+ * multiplied within Javascript's floating-point representation without overflow
+ * or change in sign.
+ *
+ * In the algorithms below, we frequently reduce the negative case to the
+ * positive case by negating the input(s) and then post-processing the result.
+ * Note that we must ALWAYS check specially whether those values are MIN_VALUE
+ * (-2^63) because -MIN_VALUE == MIN_VALUE (since 2^63 cannot be represented as
+ * a positive number, it overflows back into a negative).  Not handling this
+ * case would often result in infinite recursion.
+ *
+ * @param {number} low  The low (signed) 32 bits of the long.
+ * @param {number} high  The high (signed) 32 bits of the long.
+ * @struct
+ * @constructor
+ * @final
+ */
+goog.math.Long = function(low, high) {
+  /**
+   * @type {number}
+   * @private
+   */
+  this.low_ = low | 0;  // force into 32 signed bits.
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.high_ = high | 0;  // force into 32 signed bits.
+};
+
+
+// NOTE: Common constant values ZERO, ONE, NEG_ONE, etc. are defined below the
+// from* methods on which they depend.
+
+
+/**
+ * A cache of the Long representations of small integer values.
+ * @type {!Object<number, !goog.math.Long>}
+ * @private
+ */
+goog.math.Long.IntCache_ = {};
+
+
+/**
+ * A cache of the Long representations of common values.
+ * @type {!Object<goog.math.Long.ValueCacheId_, !goog.math.Long>}
+ * @private
+ */
+goog.math.Long.valueCache_ = {};
+
+/**
+ * Returns a cached long number representing the given (32-bit) integer value.
+ * @param {number} value The 32-bit integer in question.
+ * @return {!goog.math.Long} The corresponding Long value.
+ * @private
+ */
+goog.math.Long.getCachedIntValue_ = function(value) {
+  return goog.reflect.cache(goog.math.Long.IntCache_, value, function(val) {
+    return new goog.math.Long(val, val < 0 ? -1 : 0);
+  });
+};
+
+/**
+ * The array of maximum values of a Long in string representation for a given
+ * radix between 2 and 36, inclusive.
+ * @private @const {!Array<string>}
+ */
+goog.math.Long.MAX_VALUE_FOR_RADIX_ = [
+  '', '',  // unused
+  '111111111111111111111111111111111111111111111111111111111111111',
+  // base 2
+  '2021110011022210012102010021220101220221',  // base 3
+  '13333333333333333333333333333333',          // base 4
+  '1104332401304422434310311212',              // base 5
+  '1540241003031030222122211',                 // base 6
+  '22341010611245052052300',                   // base 7
+  '777777777777777777777',                     // base 8
+  '67404283172107811827',                      // base 9
+  '9223372036854775807',                       // base 10
+  '1728002635214590697',                       // base 11
+  '41a792678515120367',                        // base 12
+  '10b269549075433c37',                        // base 13
+  '4340724c6c71dc7a7',                         // base 14
+  '160e2ad3246366807',                         // base 15
+  '7fffffffffffffff',                          // base 16
+  '33d3d8307b214008',                          // base 17
+  '16agh595df825fa7',                          // base 18
+  'ba643dci0ffeehh',                           // base 19
+  '5cbfjia3fh26ja7',                           // base 20
+  '2heiciiie82dh97',                           // base 21
+  '1adaibb21dckfa7',                           // base 22
+  'i6k448cf4192c2',                            // base 23
+  'acd772jnc9l0l7',                            // base 24
+  '64ie1focnn5g77',                            // base 25
+  '3igoecjbmca687',                            // base 26
+  '27c48l5b37oaop',                            // base 27
+  '1bk39f3ah3dmq7',                            // base 28
+  'q1se8f0m04isb',                             // base 29
+  'hajppbc1fc207',                             // base 30
+  'bm03i95hia437',                             // base 31
+  '7vvvvvvvvvvvv',                             // base 32
+  '5hg4ck9jd4u37',                             // base 33
+  '3tdtk1v8j6tpp',                             // base 34
+  '2pijmikexrxp7',                             // base 35
+  '1y2p0ij32e8e7'                              // base 36
+];
+
+
+/**
+ * The array of minimum values of a Long in string representation for a given
+ * radix between 2 and 36, inclusive.
+ * @private @const {!Array<string>}
+ */
+goog.math.Long.MIN_VALUE_FOR_RADIX_ = [
+  '', '',  // unused
+  '-1000000000000000000000000000000000000000000000000000000000000000',
+  // base 2
+  '-2021110011022210012102010021220101220222',  // base 3
+  '-20000000000000000000000000000000',          // base 4
+  '-1104332401304422434310311213',              // base 5
+  '-1540241003031030222122212',                 // base 6
+  '-22341010611245052052301',                   // base 7
+  '-1000000000000000000000',                    // base 8
+  '-67404283172107811828',                      // base 9
+  '-9223372036854775808',                       // base 10
+  '-1728002635214590698',                       // base 11
+  '-41a792678515120368',                        // base 12
+  '-10b269549075433c38',                        // base 13
+  '-4340724c6c71dc7a8',                         // base 14
+  '-160e2ad3246366808',                         // base 15
+  '-8000000000000000',                          // base 16
+  '-33d3d8307b214009',                          // base 17
+  '-16agh595df825fa8',                          // base 18
+  '-ba643dci0ffeehi',                           // base 19
+  '-5cbfjia3fh26ja8',                           // base 20
+  '-2heiciiie82dh98',                           // base 21
+  '-1adaibb21dckfa8',                           // base 22
+  '-i6k448cf4192c3',                            // base 23
+  '-acd772jnc9l0l8',                            // base 24
+  '-64ie1focnn5g78',                            // base 25
+  '-3igoecjbmca688',                            // base 26
+  '-27c48l5b37oaoq',                            // base 27
+  '-1bk39f3ah3dmq8',                            // base 28
+  '-q1se8f0m04isc',                             // base 29
+  '-hajppbc1fc208',                             // base 30
+  '-bm03i95hia438',                             // base 31
+  '-8000000000000',                             // base 32
+  '-5hg4ck9jd4u38',                             // base 33
+  '-3tdtk1v8j6tpq',                             // base 34
+  '-2pijmikexrxp8',                             // base 35
+  '-1y2p0ij32e8e8'                              // base 36
+];
+
+
+/**
+ * Returns a Long representing the given (32-bit) integer value.
+ * @param {number} value The 32-bit integer in question.
+ * @return {!goog.math.Long} The corresponding Long value.
+ */
+goog.math.Long.fromInt = function(value) {
+  var intValue = value | 0;
+  goog.asserts.assert(value === intValue, 'value should be a 32-bit integer');
+
+  if (-128 <= intValue && intValue < 128) {
+    return goog.math.Long.getCachedIntValue_(intValue);
+  } else {
+    return new goog.math.Long(intValue, intValue < 0 ? -1 : 0);
+  }
+};
+
+
+/**
+ * Returns a Long representing the given value.
+ * NaN will be returned as zero. Infinity is converted to max value and
+ * -Infinity to min value.
+ * @param {number} value The number in question.
+ * @return {!goog.math.Long} The corresponding Long value.
+ */
+goog.math.Long.fromNumber = function(value) {
+  if (isNaN(value)) {
+    return goog.math.Long.getZero();
+  } else if (value <= -goog.math.Long.TWO_PWR_63_DBL_) {
+    return goog.math.Long.getMinValue();
+  } else if (value + 1 >= goog.math.Long.TWO_PWR_63_DBL_) {
+    return goog.math.Long.getMaxValue();
+  } else if (value < 0) {
+    return goog.math.Long.fromNumber(-value).negate();
+  } else {
+    return new goog.math.Long(
+        (value % goog.math.Long.TWO_PWR_32_DBL_) | 0,
+        (value / goog.math.Long.TWO_PWR_32_DBL_) | 0);
+  }
+};
+
+
+/**
+ * Returns a Long representing the 64-bit integer that comes by concatenating
+ * the given high and low bits.  Each is assumed to use 32 bits.
+ * @param {number} lowBits The low 32-bits.
+ * @param {number} highBits The high 32-bits.
+ * @return {!goog.math.Long} The corresponding Long value.
+ */
+goog.math.Long.fromBits = function(lowBits, highBits) {
+  return new goog.math.Long(lowBits, highBits);
+};
+
+
+/**
+ * Returns a Long representation of the given string, written using the given
+ * radix.
+ * @param {string} str The textual representation of the Long.
+ * @param {number=} opt_radix The radix in which the text is written.
+ * @return {!goog.math.Long} The corresponding Long value.
+ */
+goog.math.Long.fromString = function(str, opt_radix) {
+  if (str.length == 0) {
+    throw new Error('number format error: empty string');
+  }
+
+  var radix = opt_radix || 10;
+  if (radix < 2 || 36 < radix) {
+    throw new Error('radix out of range: ' + radix);
+  }
+
+  if (str.charAt(0) == '-') {
+    return goog.math.Long.fromString(str.substring(1), radix).negate();
+  } else if (str.indexOf('-') >= 0) {
+    throw new Error('number format error: interior "-" character: ' + str);
+  }
+
+  // Do several (8) digits each time through the loop, so as to
+  // minimize the calls to the very expensive emulated div.
+  var radixToPower = goog.math.Long.fromNumber(Math.pow(radix, 8));
+
+  var result = goog.math.Long.getZero();
+  for (var i = 0; i < str.length; i += 8) {
+    var size = Math.min(8, str.length - i);
+    var value = parseInt(str.substring(i, i + size), radix);
+    if (size < 8) {
+      var power = goog.math.Long.fromNumber(Math.pow(radix, size));
+      result = result.multiply(power).add(goog.math.Long.fromNumber(value));
+    } else {
+      result = result.multiply(radixToPower);
+      result = result.add(goog.math.Long.fromNumber(value));
+    }
+  }
+  return result;
+};
+
+/**
+ * Returns the boolean value of whether the input string is within a Long's
+ * range. Assumes an input string containing only numeric characters with an
+ * optional preceding '-'.
+ * @param {string} str The textual representation of the Long.
+ * @param {number=} opt_radix The radix in which the text is written.
+ * @return {boolean} Whether the string is within the range of a Long.
+ */
+goog.math.Long.isStringInRange = function(str, opt_radix) {
+  var radix = opt_radix || 10;
+  if (radix < 2 || 36 < radix) {
+    throw new Error('radix out of range: ' + radix);
+  }
+
+  var extremeValue = (str.charAt(0) == '-') ?
+      goog.math.Long.MIN_VALUE_FOR_RADIX_[radix] :
+      goog.math.Long.MAX_VALUE_FOR_RADIX_[radix];
+
+  if (str.length < extremeValue.length) {
+    return true;
+  } else if (str.length == extremeValue.length && str <= extremeValue) {
+    return true;
+  } else {
+    return false;
+  }
+};
+
+// NOTE: the compiler should inline these constant values below and then remove
+// these variables, so there should be no runtime penalty for these.
+
+
+/**
+ * Number used repeated below in calculations.  This must appear before the
+ * first call to any from* function below.
+ * @type {number}
+ * @private
+ */
+goog.math.Long.TWO_PWR_16_DBL_ = 1 << 16;
+
+
+/**
+ * @type {number}
+ * @private
+ */
+goog.math.Long.TWO_PWR_32_DBL_ =
+    goog.math.Long.TWO_PWR_16_DBL_ * goog.math.Long.TWO_PWR_16_DBL_;
+
+
+/**
+ * @type {number}
+ * @private
+ */
+goog.math.Long.TWO_PWR_64_DBL_ =
+    goog.math.Long.TWO_PWR_32_DBL_ * goog.math.Long.TWO_PWR_32_DBL_;
+
+
+/**
+ * @type {number}
+ * @private
+ */
+goog.math.Long.TWO_PWR_63_DBL_ = goog.math.Long.TWO_PWR_64_DBL_ / 2;
+
+
+/**
+ * @return {!goog.math.Long}
+ * @public
+ */
+goog.math.Long.getZero = function() {
+  return goog.math.Long.getCachedIntValue_(0);
+};
+
+
+/**
+ * @return {!goog.math.Long}
+ * @public
+ */
+goog.math.Long.getOne = function() {
+  return goog.math.Long.getCachedIntValue_(1);
+};
+
+
+/**
+ * @return {!goog.math.Long}
+ * @public
+ */
+goog.math.Long.getNegOne = function() {
+  return goog.math.Long.getCachedIntValue_(-1);
+};
+
+
+/**
+ * @return {!goog.math.Long}
+ * @public
+ */
+goog.math.Long.getMaxValue = function() {
+  return goog.reflect.cache(
+      goog.math.Long.valueCache_, goog.math.Long.ValueCacheId_.MAX_VALUE,
+      function() {
+        return goog.math.Long.fromBits(0xFFFFFFFF | 0, 0x7FFFFFFF | 0);
+      });
+};
+
+
+/**
+ * @return {!goog.math.Long}
+ * @public
+ */
+goog.math.Long.getMinValue = function() {
+  return goog.reflect.cache(
+      goog.math.Long.valueCache_, goog.math.Long.ValueCacheId_.MIN_VALUE,
+      function() { return goog.math.Long.fromBits(0, 0x80000000 | 0); });
+};
+
+
+/**
+ * @return {!goog.math.Long}
+ * @public
+ */
+goog.math.Long.getTwoPwr24 = function() {
+  return goog.reflect.cache(
+      goog.math.Long.valueCache_, goog.math.Long.ValueCacheId_.TWO_PWR_24,
+      function() { return goog.math.Long.fromInt(1 << 24); });
+};
+
+
+/** @return {number} The value, assuming it is a 32-bit integer. */
+goog.math.Long.prototype.toInt = function() {
+  return this.low_;
+};
+
+
+/** @return {number} The closest floating-point representation to this value. */
+goog.math.Long.prototype.toNumber = function() {
+  return this.high_ * goog.math.Long.TWO_PWR_32_DBL_ +
+      this.getLowBitsUnsigned();
+};
+
+
+/**
+ * @param {number=} opt_radix The radix in which the text should be written.
+ * @return {string} The textual representation of this value.
+ * @override
+ */
+goog.math.Long.prototype.toString = function(opt_radix) {
+  var radix = opt_radix || 10;
+  if (radix < 2 || 36 < radix) {
+    throw new Error('radix out of range: ' + radix);
+  }
+
+  if (this.isZero()) {
+    return '0';
+  }
+
+  if (this.isNegative()) {
+    if (this.equals(goog.math.Long.getMinValue())) {
+      // We need to change the Long value before it can be negated, so we remove
+      // the bottom-most digit in this base and then recurse to do the rest.
+      var radixLong = goog.math.Long.fromNumber(radix);
+      var div = this.div(radixLong);
+      var rem = div.multiply(radixLong).subtract(this);
+      return div.toString(radix) + rem.toInt().toString(radix);
+    } else {
+      return '-' + this.negate().toString(radix);
+    }
+  }
+
+  // Do several (6) digits each time through the loop, so as to
+  // minimize the calls to the very expensive emulated div.
+  var radixToPower = goog.math.Long.fromNumber(Math.pow(radix, 6));
+
+  var rem = this;
+  var result = '';
+  while (true) {
+    var remDiv = rem.div(radixToPower);
+    // The right shifting fixes negative values in the case when
+    // intval >= 2^31; for more details see
+    // https://github.com/google/closure-library/pull/498
+    var intval = rem.subtract(remDiv.multiply(radixToPower)).toInt() >>> 0;
+    var digits = intval.toString(radix);
+
+    rem = remDiv;
+    if (rem.isZero()) {
+      return digits + result;
+    } else {
+      while (digits.length < 6) {
+        digits = '0' + digits;
+      }
+      result = '' + digits + result;
+    }
+  }
+};
+
+
+/** @return {number} The high 32-bits as a signed value. */
+goog.math.Long.prototype.getHighBits = function() {
+  return this.high_;
+};
+
+
+/** @return {number} The low 32-bits as a signed value. */
+goog.math.Long.prototype.getLowBits = function() {
+  return this.low_;
+};
+
+
+/** @return {number} The low 32-bits as an unsigned value. */
+goog.math.Long.prototype.getLowBitsUnsigned = function() {
+  return (this.low_ >= 0) ? this.low_ :
+                            goog.math.Long.TWO_PWR_32_DBL_ + this.low_;
+};
+
+
+/**
+ * @return {number} Returns the number of bits needed to represent the absolute
+ *     value of this Long.
+ */
+goog.math.Long.prototype.getNumBitsAbs = function() {
+  if (this.isNegative()) {
+    if (this.equals(goog.math.Long.getMinValue())) {
+      return 64;
+    } else {
+      return this.negate().getNumBitsAbs();
+    }
+  } else {
+    var val = this.high_ != 0 ? this.high_ : this.low_;
+    for (var bit = 31; bit > 0; bit--) {
+      if ((val & (1 << bit)) != 0) {
+        break;
+      }
+    }
+    return this.high_ != 0 ? bit + 33 : bit + 1;
+  }
+};
+
+
+/** @return {boolean} Whether this value is zero. */
+goog.math.Long.prototype.isZero = function() {
+  return this.high_ == 0 && this.low_ == 0;
+};
+
+
+/** @return {boolean} Whether this value is negative. */
+goog.math.Long.prototype.isNegative = function() {
+  return this.high_ < 0;
+};
+
+
+/** @return {boolean} Whether this value is odd. */
+goog.math.Long.prototype.isOdd = function() {
+  return (this.low_ & 1) == 1;
+};
+
+
+/**
+ * @param {goog.math.Long} other Long to compare against.
+ * @return {boolean} Whether this Long equals the other.
+ */
+goog.math.Long.prototype.equals = function(other) {
+  return (this.high_ == other.high_) && (this.low_ == other.low_);
+};
+
+
+/**
+ * @param {goog.math.Long} other Long to compare against.
+ * @return {boolean} Whether this Long does not equal the other.
+ */
+goog.math.Long.prototype.notEquals = function(other) {
+  return (this.high_ != other.high_) || (this.low_ != other.low_);
+};
+
+
+/**
+ * @param {goog.math.Long} other Long to compare against.
+ * @return {boolean} Whether this Long is less than the other.
+ */
+goog.math.Long.prototype.lessThan = function(other) {
+  return this.compare(other) < 0;
+};
+
+
+/**
+ * @param {goog.math.Long} other Long to compare against.
+ * @return {boolean} Whether this Long is less than or equal to the other.
+ */
+goog.math.Long.prototype.lessThanOrEqual = function(other) {
+  return this.compare(other) <= 0;
+};
+
+
+/**
+ * @param {goog.math.Long} other Long to compare against.
+ * @return {boolean} Whether this Long is greater than the other.
+ */
+goog.math.Long.prototype.greaterThan = function(other) {
+  return this.compare(other) > 0;
+};
+
+
+/**
+ * @param {goog.math.Long} other Long to compare against.
+ * @return {boolean} Whether this Long is greater than or equal to the other.
+ */
+goog.math.Long.prototype.greaterThanOrEqual = function(other) {
+  return this.compare(other) >= 0;
+};
+
+
+/**
+ * Compares this Long with the given one.
+ * @param {goog.math.Long} other Long to compare against.
+ * @return {number} 0 if they are the same, 1 if the this is greater, and -1
+ *     if the given one is greater.
+ */
+goog.math.Long.prototype.compare = function(other) {
+  if (this.equals(other)) {
+    return 0;
+  }
+
+  var thisNeg = this.isNegative();
+  var otherNeg = other.isNegative();
+  if (thisNeg && !otherNeg) {
+    return -1;
+  }
+  if (!thisNeg && otherNeg) {
+    return 1;
+  }
+
+  // at this point, the signs are the same, so subtraction will not overflow
+  if (this.subtract(other).isNegative()) {
+    return -1;
+  } else {
+    return 1;
+  }
+};
+
+
+/** @return {!goog.math.Long} The negation of this value. */
+goog.math.Long.prototype.negate = function() {
+  if (this.equals(goog.math.Long.getMinValue())) {
+    return goog.math.Long.getMinValue();
+  } else {
+    return this.not().add(goog.math.Long.getOne());
+  }
+};
+
+
+/**
+ * Returns the sum of this and the given Long.
+ * @param {goog.math.Long} other Long to add to this one.
+ * @return {!goog.math.Long} The sum of this and the given Long.
+ */
+goog.math.Long.prototype.add = function(other) {
+  // Divide each number into 4 chunks of 16 bits, and then sum the chunks.
+
+  var a48 = this.high_ >>> 16;
+  var a32 = this.high_ & 0xFFFF;
+  var a16 = this.low_ >>> 16;
+  var a00 = this.low_ & 0xFFFF;
+
+  var b48 = other.high_ >>> 16;
+  var b32 = other.high_ & 0xFFFF;
+  var b16 = other.low_ >>> 16;
+  var b00 = other.low_ & 0xFFFF;
+
+  var c48 = 0, c32 = 0, c16 = 0, c00 = 0;
+  c00 += a00 + b00;
+  c16 += c00 >>> 16;
+  c00 &= 0xFFFF;
+  c16 += a16 + b16;
+  c32 += c16 >>> 16;
+  c16 &= 0xFFFF;
+  c32 += a32 + b32;
+  c48 += c32 >>> 16;
+  c32 &= 0xFFFF;
+  c48 += a48 + b48;
+  c48 &= 0xFFFF;
+  return goog.math.Long.fromBits((c16 << 16) | c00, (c48 << 16) | c32);
+};
+
+
+/**
+ * Returns the difference of this and the given Long.
+ * @param {goog.math.Long} other Long to subtract from this.
+ * @return {!goog.math.Long} The difference of this and the given Long.
+ */
+goog.math.Long.prototype.subtract = function(other) {
+  return this.add(other.negate());
+};
+
+
+/**
+ * Returns the product of this and the given long.
+ * @param {goog.math.Long} other Long to multiply with this.
+ * @return {!goog.math.Long} The product of this and the other.
+ */
+goog.math.Long.prototype.multiply = function(other) {
+  if (this.isZero()) {
+    return goog.math.Long.getZero();
+  } else if (other.isZero()) {
+    return goog.math.Long.getZero();
+  }
+
+  if (this.equals(goog.math.Long.getMinValue())) {
+    return other.isOdd() ? goog.math.Long.getMinValue() :
+                           goog.math.Long.getZero();
+  } else if (other.equals(goog.math.Long.getMinValue())) {
+    return this.isOdd() ? goog.math.Long.getMinValue() :
+                          goog.math.Long.getZero();
+  }
+
+  if (this.isNegative()) {
+    if (other.isNegative()) {
+      return this.negate().multiply(other.negate());
+    } else {
+      return this.negate().multiply(other).negate();
+    }
+  } else if (other.isNegative()) {
+    return this.multiply(other.negate()).negate();
+  }
+
+  // If both longs are small, use float multiplication
+  if (this.lessThan(goog.math.Long.getTwoPwr24()) &&
+      other.lessThan(goog.math.Long.getTwoPwr24())) {
+    return goog.math.Long.fromNumber(this.toNumber() * other.toNumber());
+  }
+
+  // Divide each long into 4 chunks of 16 bits, and then add up 4x4 products.
+  // We can skip products that would overflow.
+
+  var a48 = this.high_ >>> 16;
+  var a32 = this.high_ & 0xFFFF;
+  var a16 = this.low_ >>> 16;
+  var a00 = this.low_ & 0xFFFF;
+
+  var b48 = other.high_ >>> 16;
+  var b32 = other.high_ & 0xFFFF;
+  var b16 = other.low_ >>> 16;
+  var b00 = other.low_ & 0xFFFF;
+
+  var c48 = 0, c32 = 0, c16 = 0, c00 = 0;
+  c00 += a00 * b00;
+  c16 += c00 >>> 16;
+  c00 &= 0xFFFF;
+  c16 += a16 * b00;
+  c32 += c16 >>> 16;
+  c16 &= 0xFFFF;
+  c16 += a00 * b16;
+  c32 += c16 >>> 16;
+  c16 &= 0xFFFF;
+  c32 += a32 * b00;
+  c48 += c32 >>> 16;
+  c32 &= 0xFFFF;
+  c32 += a16 * b16;
+  c48 += c32 >>> 16;
+  c32 &= 0xFFFF;
+  c32 += a00 * b32;
+  c48 += c32 >>> 16;
+  c32 &= 0xFFFF;
+  c48 += a48 * b00 + a32 * b16 + a16 * b32 + a00 * b48;
+  c48 &= 0xFFFF;
+  return goog.math.Long.fromBits((c16 << 16) | c00, (c48 << 16) | c32);
+};
+
+
+/**
+ * Returns this Long divided by the given one.
+ * @param {goog.math.Long} other Long by which to divide.
+ * @return {!goog.math.Long} This Long divided by the given one.
+ */
+goog.math.Long.prototype.div = function(other) {
+  if (other.isZero()) {
+    throw new Error('division by zero');
+  } else if (this.isZero()) {
+    return goog.math.Long.getZero();
+  }
+
+  if (this.equals(goog.math.Long.getMinValue())) {
+    if (other.equals(goog.math.Long.getOne()) ||
+        other.equals(goog.math.Long.getNegOne())) {
+      return goog.math.Long.getMinValue();  // recall -MIN_VALUE == MIN_VALUE
+    } else if (other.equals(goog.math.Long.getMinValue())) {
+      return goog.math.Long.getOne();
+    } else {
+      // At this point, we have |other| >= 2, so |this/other| < |MIN_VALUE|.
+      var halfThis = this.shiftRight(1);
+      var approx = halfThis.div(other).shiftLeft(1);
+      if (approx.equals(goog.math.Long.getZero())) {
+        return other.isNegative() ? goog.math.Long.getOne() :
+                                    goog.math.Long.getNegOne();
+      } else {
+        var rem = this.subtract(other.multiply(approx));
+        var result = approx.add(rem.div(other));
+        return result;
+      }
+    }
+  } else if (other.equals(goog.math.Long.getMinValue())) {
+    return goog.math.Long.getZero();
+  }
+
+  if (this.isNegative()) {
+    if (other.isNegative()) {
+      return this.negate().div(other.negate());
+    } else {
+      return this.negate().div(other).negate();
+    }
+  } else if (other.isNegative()) {
+    return this.div(other.negate()).negate();
+  }
+
+  // Repeat the following until the remainder is less than other:  find a
+  // floating-point that approximates remainder / other *from below*, add this
+  // into the result, and subtract it from the remainder.  It is critical that
+  // the approximate value is less than or equal to the real value so that the
+  // remainder never becomes negative.
+  var res = goog.math.Long.getZero();
+  var rem = this;
+  while (rem.greaterThanOrEqual(other)) {
+    // Approximate the result of division. This may be a little greater or
+    // smaller than the actual value.
+    var approx = Math.max(1, Math.floor(rem.toNumber() / other.toNumber()));
+
+    // We will tweak the approximate result by changing it in the 48-th digit or
+    // the smallest non-fractional digit, whichever is larger.
+    var log2 = Math.ceil(Math.log(approx) / Math.LN2);
+    var delta = (log2 <= 48) ? 1 : Math.pow(2, log2 - 48);
+
+    // Decrease the approximation until it is smaller than the remainder.  Note
+    // that if it is too large, the product overflows and is negative.
+    var approxRes = goog.math.Long.fromNumber(approx);
+    var approxRem = approxRes.multiply(other);
+    while (approxRem.isNegative() || approxRem.greaterThan(rem)) {
+      approx -= delta;
+      approxRes = goog.math.Long.fromNumber(approx);
+      approxRem = approxRes.multiply(other);
+    }
+
+    // We know the answer can't be zero... and actually, zero would cause
+    // infinite recursion since we would make no progress.
+    if (approxRes.isZero()) {
+      approxRes = goog.math.Long.getOne();
+    }
+
+    res = res.add(approxRes);
+    rem = rem.subtract(approxRem);
+  }
+  return res;
+};
+
+
+/**
+ * Returns this Long modulo the given one.
+ * @param {goog.math.Long} other Long by which to mod.
+ * @return {!goog.math.Long} This Long modulo the given one.
+ */
+goog.math.Long.prototype.modulo = function(other) {
+  return this.subtract(this.div(other).multiply(other));
+};
+
+
+/** @return {!goog.math.Long} The bitwise-NOT of this value. */
+goog.math.Long.prototype.not = function() {
+  return goog.math.Long.fromBits(~this.low_, ~this.high_);
+};
+
+
+/**
+ * Returns the bitwise-AND of this Long and the given one.
+ * @param {goog.math.Long} other The Long with which to AND.
+ * @return {!goog.math.Long} The bitwise-AND of this and the other.
+ */
+goog.math.Long.prototype.and = function(other) {
+  return goog.math.Long.fromBits(
+      this.low_ & other.low_, this.high_ & other.high_);
+};
+
+
+/**
+ * Returns the bitwise-OR of this Long and the given one.
+ * @param {goog.math.Long} other The Long with which to OR.
+ * @return {!goog.math.Long} The bitwise-OR of this and the other.
+ */
+goog.math.Long.prototype.or = function(other) {
+  return goog.math.Long.fromBits(
+      this.low_ | other.low_, this.high_ | other.high_);
+};
+
+
+/**
+ * Returns the bitwise-XOR of this Long and the given one.
+ * @param {goog.math.Long} other The Long with which to XOR.
+ * @return {!goog.math.Long} The bitwise-XOR of this and the other.
+ */
+goog.math.Long.prototype.xor = function(other) {
+  return goog.math.Long.fromBits(
+      this.low_ ^ other.low_, this.high_ ^ other.high_);
+};
+
+
+/**
+ * Returns this Long with bits shifted to the left by the given amount.
+ * @param {number} numBits The number of bits by which to shift.
+ * @return {!goog.math.Long} This shifted to the left by the given amount.
+ */
+goog.math.Long.prototype.shiftLeft = function(numBits) {
+  numBits &= 63;
+  if (numBits == 0) {
+    return this;
+  } else {
+    var low = this.low_;
+    if (numBits < 32) {
+      var high = this.high_;
+      return goog.math.Long.fromBits(
+          low << numBits, (high << numBits) | (low >>> (32 - numBits)));
+    } else {
+      return goog.math.Long.fromBits(0, low << (numBits - 32));
+    }
+  }
+};
+
+
+/**
+ * Returns this Long with bits shifted to the right by the given amount.
+ * The new leading bits match the current sign bit.
+ * @param {number} numBits The number of bits by which to shift.
+ * @return {!goog.math.Long} This shifted to the right by the given amount.
+ */
+goog.math.Long.prototype.shiftRight = function(numBits) {
+  numBits &= 63;
+  if (numBits == 0) {
+    return this;
+  } else {
+    var high = this.high_;
+    if (numBits < 32) {
+      var low = this.low_;
+      return goog.math.Long.fromBits(
+          (low >>> numBits) | (high << (32 - numBits)), high >> numBits);
+    } else {
+      return goog.math.Long.fromBits(
+          high >> (numBits - 32), high >= 0 ? 0 : -1);
+    }
+  }
+};
+
+
+/**
+ * Returns this Long with bits shifted to the right by the given amount, with
+ * zeros placed into the new leading bits.
+ * @param {number} numBits The number of bits by which to shift.
+ * @return {!goog.math.Long} This shifted to the right by the given amount, with
+ *     zeros placed into the new leading bits.
+ */
+goog.math.Long.prototype.shiftRightUnsigned = function(numBits) {
+  numBits &= 63;
+  if (numBits == 0) {
+    return this;
+  } else {
+    var high = this.high_;
+    if (numBits < 32) {
+      var low = this.low_;
+      return goog.math.Long.fromBits(
+          (low >>> numBits) | (high << (32 - numBits)), high >>> numBits);
+    } else if (numBits == 32) {
+      return goog.math.Long.fromBits(high, 0);
+    } else {
+      return goog.math.Long.fromBits(high >>> (numBits - 32), 0);
+    }
+  }
+};
+
+
+/**
+ * @enum {number} Ids of commonly requested Long instances.
+ * @private
+ */
+goog.math.Long.ValueCacheId_ = {
+  MAX_VALUE: 1,
+  MIN_VALUE: 2,
+  TWO_PWR_24: 6
+};
diff --git a/third_party/ink/closure/math/math.js b/third_party/ink/closure/math/math.js
new file mode 100644
index 0000000..a251005b
--- /dev/null
+++ b/third_party/ink/closure/math/math.js
@@ -0,0 +1,449 @@
+// Copyright 2006 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 Additional mathematical functions.
+ * @author pupius@google.com (Daniel Pupius)
+ */
+
+goog.provide('goog.math');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+
+
+/**
+ * Returns a random integer greater than or equal to 0 and less than {@code a}.
+ * @param {number} a  The upper bound for the random integer (exclusive).
+ * @return {number} A random integer N such that 0 <= N < a.
+ */
+goog.math.randomInt = function(a) {
+  return Math.floor(Math.random() * a);
+};
+
+
+/**
+ * Returns a random number greater than or equal to {@code a} and less than
+ * {@code b}.
+ * @param {number} a  The lower bound for the random number (inclusive).
+ * @param {number} b  The upper bound for the random number (exclusive).
+ * @return {number} A random number N such that a <= N < b.
+ */
+goog.math.uniformRandom = function(a, b) {
+  return a + Math.random() * (b - a);
+};
+
+
+/**
+ * Takes a number and clamps it to within the provided bounds.
+ * @param {number} value The input number.
+ * @param {number} min The minimum value to return.
+ * @param {number} max The maximum value to return.
+ * @return {number} The input number if it is within bounds, or the nearest
+ *     number within the bounds.
+ */
+goog.math.clamp = function(value, min, max) {
+  return Math.min(Math.max(value, min), max);
+};
+
+
+/**
+ * The % operator in JavaScript returns the remainder of a / b, but differs from
+ * some other languages in that the result will have the same sign as the
+ * dividend. For example, -1 % 8 == -1, whereas in some other languages
+ * (such as Python) the result would be 7. This function emulates the more
+ * correct modulo behavior, which is useful for certain applications such as
+ * calculating an offset index in a circular list.
+ *
+ * @param {number} a The dividend.
+ * @param {number} b The divisor.
+ * @return {number} a % b where the result is between 0 and b (either 0 <= x < b
+ *     or b < x <= 0, depending on the sign of b).
+ */
+goog.math.modulo = function(a, b) {
+  var r = a % b;
+  // If r and b differ in sign, add b to wrap the result to the correct sign.
+  return (r * b < 0) ? r + b : r;
+};
+
+
+/**
+ * Performs linear interpolation between values a and b. Returns the value
+ * between a and b proportional to x (when x is between 0 and 1. When x is
+ * outside this range, the return value is a linear extrapolation).
+ * @param {number} a A number.
+ * @param {number} b A number.
+ * @param {number} x The proportion between a and b.
+ * @return {number} The interpolated value between a and b.
+ */
+goog.math.lerp = function(a, b, x) {
+  return a + x * (b - a);
+};
+
+
+/**
+ * Tests whether the two values are equal to each other, within a certain
+ * tolerance to adjust for floating point errors.
+ * @param {number} a A number.
+ * @param {number} b A number.
+ * @param {number=} opt_tolerance Optional tolerance range. Defaults
+ *     to 0.000001. If specified, should be greater than 0.
+ * @return {boolean} Whether {@code a} and {@code b} are nearly equal.
+ */
+goog.math.nearlyEquals = function(a, b, opt_tolerance) {
+  return Math.abs(a - b) <= (opt_tolerance || 0.000001);
+};
+
+
+// TODO(jrajeshwar): Rename to normalizeAngle, retaining old name as deprecated
+// alias.
+/**
+ * Normalizes an angle to be in range [0-360). Angles outside this range will
+ * be normalized to be the equivalent angle with that range.
+ * @param {number} angle Angle in degrees.
+ * @return {number} Standardized angle.
+ */
+goog.math.standardAngle = function(angle) {
+  return goog.math.modulo(angle, 360);
+};
+
+
+/**
+ * Normalizes an angle to be in range [0-2*PI). Angles outside this range will
+ * be normalized to be the equivalent angle with that range.
+ * @param {number} angle Angle in radians.
+ * @return {number} Standardized angle.
+ */
+goog.math.standardAngleInRadians = function(angle) {
+  return goog.math.modulo(angle, 2 * Math.PI);
+};
+
+
+/**
+ * Converts degrees to radians.
+ * @param {number} angleDegrees Angle in degrees.
+ * @return {number} Angle in radians.
+ */
+goog.math.toRadians = function(angleDegrees) {
+  return angleDegrees * Math.PI / 180;
+};
+
+
+/**
+ * Converts radians to degrees.
+ * @param {number} angleRadians Angle in radians.
+ * @return {number} Angle in degrees.
+ */
+goog.math.toDegrees = function(angleRadians) {
+  return angleRadians * 180 / Math.PI;
+};
+
+
+/**
+ * For a given angle and radius, finds the X portion of the offset.
+ * @param {number} degrees Angle in degrees (zero points in +X direction).
+ * @param {number} radius Radius.
+ * @return {number} The x-distance for the angle and radius.
+ */
+goog.math.angleDx = function(degrees, radius) {
+  return radius * Math.cos(goog.math.toRadians(degrees));
+};
+
+
+/**
+ * For a given angle and radius, finds the Y portion of the offset.
+ * @param {number} degrees Angle in degrees (zero points in +X direction).
+ * @param {number} radius Radius.
+ * @return {number} The y-distance for the angle and radius.
+ */
+goog.math.angleDy = function(degrees, radius) {
+  return radius * Math.sin(goog.math.toRadians(degrees));
+};
+
+
+/**
+ * Computes the angle between two points (x1,y1) and (x2,y2).
+ * Angle zero points in the +X direction, 90 degrees points in the +Y
+ * direction (down) and from there we grow clockwise towards 360 degrees.
+ * @param {number} x1 x of first point.
+ * @param {number} y1 y of first point.
+ * @param {number} x2 x of second point.
+ * @param {number} y2 y of second point.
+ * @return {number} Standardized angle in degrees of the vector from
+ *     x1,y1 to x2,y2.
+ */
+goog.math.angle = function(x1, y1, x2, y2) {
+  return goog.math.standardAngle(
+      goog.math.toDegrees(Math.atan2(y2 - y1, x2 - x1)));
+};
+
+
+/**
+ * Computes the difference between startAngle and endAngle (angles in degrees).
+ * @param {number} startAngle  Start angle in degrees.
+ * @param {number} endAngle  End angle in degrees.
+ * @return {number} The number of degrees that when added to
+ *     startAngle will result in endAngle. Positive numbers mean that the
+ *     direction is clockwise. Negative numbers indicate a counter-clockwise
+ *     direction.
+ *     The shortest route (clockwise vs counter-clockwise) between the angles
+ *     is used.
+ *     When the difference is 180 degrees, the function returns 180 (not -180)
+ *     angleDifference(30, 40) is 10, and angleDifference(40, 30) is -10.
+ *     angleDifference(350, 10) is 20, and angleDifference(10, 350) is -20.
+ */
+goog.math.angleDifference = function(startAngle, endAngle) {
+  var d =
+      goog.math.standardAngle(endAngle) - goog.math.standardAngle(startAngle);
+  if (d > 180) {
+    d = d - 360;
+  } else if (d <= -180) {
+    d = 360 + d;
+  }
+  return d;
+};
+
+
+/**
+ * Returns the sign of a number as per the "sign" or "signum" function.
+ * @param {number} x The number to take the sign of.
+ * @return {number} -1 when negative, 1 when positive, 0 when 0. Preserves
+ *     signed zeros and NaN.
+ */
+goog.math.sign = function(x) {
+  if (x > 0) {
+    return 1;
+  }
+  if (x < 0) {
+    return -1;
+  }
+  return x;  // Preserves signed zeros and NaN.
+};
+
+
+/**
+ * JavaScript implementation of Longest Common Subsequence problem.
+ * http://en.wikipedia.org/wiki/Longest_common_subsequence
+ *
+ * Returns the longest possible array that is subarray of both of given arrays.
+ *
+ * @param {IArrayLike<S>} array1 First array of objects.
+ * @param {IArrayLike<T>} array2 Second array of objects.
+ * @param {Function=} opt_compareFn Function that acts as a custom comparator
+ *     for the array ojects. Function should return true if objects are equal,
+ *     otherwise false.
+ * @param {Function=} opt_collectorFn Function used to decide what to return
+ *     as a result subsequence. It accepts 2 arguments: index of common element
+ *     in the first array and index in the second. The default function returns
+ *     element from the first array.
+ * @return {!Array<S|T>} A list of objects that are common to both arrays
+ *     such that there is no common subsequence with size greater than the
+ *     length of the list.
+ * @template S,T
+ */
+goog.math.longestCommonSubsequence = function(
+    array1, array2, opt_compareFn, opt_collectorFn) {
+
+  var compare = opt_compareFn || function(a, b) { return a == b; };
+
+  var collect = opt_collectorFn || function(i1, i2) { return array1[i1]; };
+
+  var length1 = array1.length;
+  var length2 = array2.length;
+
+  var arr = [];
+  for (var i = 0; i < length1 + 1; i++) {
+    arr[i] = [];
+    arr[i][0] = 0;
+  }
+
+  for (var j = 0; j < length2 + 1; j++) {
+    arr[0][j] = 0;
+  }
+
+  for (i = 1; i <= length1; i++) {
+    for (j = 1; j <= length2; j++) {
+      if (compare(array1[i - 1], array2[j - 1])) {
+        arr[i][j] = arr[i - 1][j - 1] + 1;
+      } else {
+        arr[i][j] = Math.max(arr[i - 1][j], arr[i][j - 1]);
+      }
+    }
+  }
+
+  // Backtracking
+  var result = [];
+  var i = length1, j = length2;
+  while (i > 0 && j > 0) {
+    if (compare(array1[i - 1], array2[j - 1])) {
+      result.unshift(collect(i - 1, j - 1));
+      i--;
+      j--;
+    } else {
+      if (arr[i - 1][j] > arr[i][j - 1]) {
+        i--;
+      } else {
+        j--;
+      }
+    }
+  }
+
+  return result;
+};
+
+
+/**
+ * Returns the sum of the arguments.
+ * @param {...number} var_args Numbers to add.
+ * @return {number} The sum of the arguments (0 if no arguments were provided,
+ *     {@code NaN} if any of the arguments is not a valid number).
+ */
+goog.math.sum = function(var_args) {
+  return /** @type {number} */ (
+      goog.array.reduce(
+          arguments, function(sum, value) { return sum + value; }, 0));
+};
+
+
+/**
+ * Returns the arithmetic mean of the arguments.
+ * @param {...number} var_args Numbers to average.
+ * @return {number} The average of the arguments ({@code NaN} if no arguments
+ *     were provided or any of the arguments is not a valid number).
+ */
+goog.math.average = function(var_args) {
+  return goog.math.sum.apply(null, arguments) / arguments.length;
+};
+
+
+/**
+ * Returns the unbiased sample variance of the arguments. For a definition,
+ * see e.g. http://en.wikipedia.org/wiki/Variance
+ * @param {...number} var_args Number samples to analyze.
+ * @return {number} The unbiased sample variance of the arguments (0 if fewer
+ *     than two samples were provided, or {@code NaN} if any of the samples is
+ *     not a valid number).
+ */
+goog.math.sampleVariance = function(var_args) {
+  var sampleSize = arguments.length;
+  if (sampleSize < 2) {
+    return 0;
+  }
+
+  var mean = goog.math.average.apply(null, arguments);
+  var variance =
+      goog.math.sum.apply(null, goog.array.map(arguments, function(val) {
+        return Math.pow(val - mean, 2);
+      })) / (sampleSize - 1);
+
+  return variance;
+};
+
+
+/**
+ * Returns the sample standard deviation of the arguments.  For a definition of
+ * sample standard deviation, see e.g.
+ * http://en.wikipedia.org/wiki/Standard_deviation
+ * @param {...number} var_args Number samples to analyze.
+ * @return {number} The sample standard deviation of the arguments (0 if fewer
+ *     than two samples were provided, or {@code NaN} if any of the samples is
+ *     not a valid number).
+ */
+goog.math.standardDeviation = function(var_args) {
+  return Math.sqrt(goog.math.sampleVariance.apply(null, arguments));
+};
+
+
+/**
+ * Returns whether the supplied number represents an integer, i.e. that is has
+ * no fractional component.  No range-checking is performed on the number.
+ * @param {number} num The number to test.
+ * @return {boolean} Whether {@code num} is an integer.
+ */
+goog.math.isInt = function(num) {
+  return isFinite(num) && num % 1 == 0;
+};
+
+
+/**
+ * Returns whether the supplied number is finite and not NaN.
+ * @param {number} num The number to test.
+ * @return {boolean} Whether {@code num} is a finite number.
+ * @deprecated Use {@link isFinite} instead.
+ */
+goog.math.isFiniteNumber = function(num) {
+  return isFinite(num);
+};
+
+
+/**
+ * @param {number} num The number to test.
+ * @return {boolean} Whether it is negative zero.
+ */
+goog.math.isNegativeZero = function(num) {
+  return num == 0 && 1 / num < 0;
+};
+
+
+/**
+ * Returns the precise value of floor(log10(num)).
+ * Simpler implementations didn't work because of floating point rounding
+ * errors. For example
+ * <ul>
+ * <li>Math.floor(Math.log(num) / Math.LN10) is off by one for num == 1e+3.
+ * <li>Math.floor(Math.log(num) * Math.LOG10E) is off by one for num == 1e+15.
+ * <li>Math.floor(Math.log10(num)) is off by one for num == 1e+15 - 1.
+ * </ul>
+ * @param {number} num A floating point number.
+ * @return {number} Its logarithm to base 10 rounded down to the nearest
+ *     integer if num > 0. -Infinity if num == 0. NaN if num < 0.
+ */
+goog.math.log10Floor = function(num) {
+  if (num > 0) {
+    var x = Math.round(Math.log(num) * Math.LOG10E);
+    return x - (parseFloat('1e' + x) > num ? 1 : 0);
+  }
+  return num == 0 ? -Infinity : NaN;
+};
+
+
+/**
+ * A tweaked variant of {@code Math.floor} which tolerates if the passed number
+ * is infinitesimally smaller than the closest integer. It often happens with
+ * the results of floating point calculations because of the finite precision
+ * of the intermediate results. For example {@code Math.floor(Math.log(1000) /
+ * Math.LN10) == 2}, not 3 as one would expect.
+ * @param {number} num A number.
+ * @param {number=} opt_epsilon An infinitesimally small positive number, the
+ *     rounding error to tolerate.
+ * @return {number} The largest integer less than or equal to {@code num}.
+ */
+goog.math.safeFloor = function(num, opt_epsilon) {
+  goog.asserts.assert(!goog.isDef(opt_epsilon) || opt_epsilon > 0);
+  return Math.floor(num + (opt_epsilon || 2e-15));
+};
+
+
+/**
+ * A tweaked variant of {@code Math.ceil}. See {@code goog.math.safeFloor} for
+ * details.
+ * @param {number} num A number.
+ * @param {number=} opt_epsilon An infinitesimally small positive number, the
+ *     rounding error to tolerate.
+ * @return {number} The smallest integer greater than or equal to {@code num}.
+ */
+goog.math.safeCeil = function(num, opt_epsilon) {
+  goog.asserts.assert(!goog.isDef(opt_epsilon) || opt_epsilon > 0);
+  return Math.ceil(num - (opt_epsilon || 2e-15));
+};
diff --git a/third_party/ink/closure/math/rect.js b/third_party/ink/closure/math/rect.js
new file mode 100644
index 0000000..28bf840
--- /dev/null
+++ b/third_party/ink/closure/math/rect.js
@@ -0,0 +1,478 @@
+// Copyright 2006 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 A utility class for representing rectangles. Some of these
+ * functions should be migrated over to non-nullable params.
+ * @author pupius@google.com (Daniel Pupius)
+ */
+
+goog.provide('goog.math.Rect');
+
+goog.require('goog.asserts');
+goog.require('goog.math.Box');
+goog.require('goog.math.Coordinate');
+goog.require('goog.math.IRect');
+goog.require('goog.math.Size');
+
+
+
+/**
+ * Class for representing rectangular regions.
+ * @param {number} x Left.
+ * @param {number} y Top.
+ * @param {number} w Width.
+ * @param {number} h Height.
+ * @struct
+ * @constructor
+ * @implements {goog.math.IRect}
+ */
+goog.math.Rect = function(x, y, w, h) {
+  /** @type {number} */
+  this.left = x;
+
+  /** @type {number} */
+  this.top = y;
+
+  /** @type {number} */
+  this.width = w;
+
+  /** @type {number} */
+  this.height = h;
+};
+
+
+/**
+ * @return {!goog.math.Rect} A new copy of this Rectangle.
+ */
+goog.math.Rect.prototype.clone = function() {
+  return new goog.math.Rect(this.left, this.top, this.width, this.height);
+};
+
+
+/**
+ * Returns a new Box object with the same position and dimensions as this
+ * rectangle.
+ * @return {!goog.math.Box} A new Box representation of this Rectangle.
+ */
+goog.math.Rect.prototype.toBox = function() {
+  var right = this.left + this.width;
+  var bottom = this.top + this.height;
+  return new goog.math.Box(this.top, right, bottom, this.left);
+};
+
+
+/**
+ * Creates a new Rect object with the position and size given.
+ * @param {!goog.math.Coordinate} position The top-left coordinate of the Rect
+ * @param {!goog.math.Size} size The size of the Rect
+ * @return {!goog.math.Rect} A new Rect initialized with the given position and
+ *     size.
+ */
+goog.math.Rect.createFromPositionAndSize = function(position, size) {
+  return new goog.math.Rect(position.x, position.y, size.width, size.height);
+};
+
+
+/**
+ * Creates a new Rect object with the same position and dimensions as a given
+ * Box.  Note that this is only the inverse of toBox if left/top are defined.
+ * @param {goog.math.Box} box A box.
+ * @return {!goog.math.Rect} A new Rect initialized with the box's position
+ *     and size.
+ */
+goog.math.Rect.createFromBox = function(box) {
+  return new goog.math.Rect(
+      box.left, box.top, box.right - box.left, box.bottom - box.top);
+};
+
+
+if (goog.DEBUG) {
+  /**
+   * Returns a nice string representing size and dimensions of rectangle.
+   * @return {string} In the form (50, 73 - 75w x 25h).
+   * @override
+   */
+  goog.math.Rect.prototype.toString = function() {
+    return '(' + this.left + ', ' + this.top + ' - ' + this.width + 'w x ' +
+        this.height + 'h)';
+  };
+}
+
+
+/**
+ * Compares rectangles for equality.
+ * @param {goog.math.IRect} a A Rectangle.
+ * @param {goog.math.IRect} b A Rectangle.
+ * @return {boolean} True iff the rectangles have the same left, top, width,
+ *     and height, or if both are null.
+ */
+goog.math.Rect.equals = function(a, b) {
+  if (a == b) {
+    return true;
+  }
+  if (!a || !b) {
+    return false;
+  }
+  return a.left == b.left && a.width == b.width && a.top == b.top &&
+      a.height == b.height;
+};
+
+
+/**
+ * Computes the intersection of this rectangle and the rectangle parameter.  If
+ * there is no intersection, returns false and leaves this rectangle as is.
+ * @param {goog.math.IRect} rect A Rectangle.
+ * @return {boolean} True iff this rectangle intersects with the parameter.
+ */
+goog.math.Rect.prototype.intersection = function(rect) {
+  var x0 = Math.max(this.left, rect.left);
+  var x1 = Math.min(this.left + this.width, rect.left + rect.width);
+
+  if (x0 <= x1) {
+    var y0 = Math.max(this.top, rect.top);
+    var y1 = Math.min(this.top + this.height, rect.top + rect.height);
+
+    if (y0 <= y1) {
+      this.left = x0;
+      this.top = y0;
+      this.width = x1 - x0;
+      this.height = y1 - y0;
+
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Returns the intersection of two rectangles. Two rectangles intersect if they
+ * touch at all, for example, two zero width and height rectangles would
+ * intersect if they had the same top and left.
+ * @param {goog.math.IRect} a A Rectangle.
+ * @param {goog.math.IRect} b A Rectangle.
+ * @return {goog.math.Rect} A new intersection rect (even if width and height
+ *     are 0), or null if there is no intersection.
+ */
+goog.math.Rect.intersection = function(a, b) {
+  // There is no nice way to do intersection via a clone, because any such
+  // clone might be unnecessary if this function returns null.  So, we duplicate
+  // code from above.
+
+  var x0 = Math.max(a.left, b.left);
+  var x1 = Math.min(a.left + a.width, b.left + b.width);
+
+  if (x0 <= x1) {
+    var y0 = Math.max(a.top, b.top);
+    var y1 = Math.min(a.top + a.height, b.top + b.height);
+
+    if (y0 <= y1) {
+      return new goog.math.Rect(x0, y0, x1 - x0, y1 - y0);
+    }
+  }
+  return null;
+};
+
+
+/**
+ * Returns whether two rectangles intersect. Two rectangles intersect if they
+ * touch at all, for example, two zero width and height rectangles would
+ * intersect if they had the same top and left.
+ * @param {goog.math.IRect} a A Rectangle.
+ * @param {goog.math.IRect} b A Rectangle.
+ * @return {boolean} Whether a and b intersect.
+ */
+goog.math.Rect.intersects = function(a, b) {
+  return (
+      a.left <= b.left + b.width && b.left <= a.left + a.width &&
+      a.top <= b.top + b.height && b.top <= a.top + a.height);
+};
+
+
+/**
+ * Returns whether a rectangle intersects this rectangle.
+ * @param {goog.math.IRect} rect A rectangle.
+ * @return {boolean} Whether rect intersects this rectangle.
+ */
+goog.math.Rect.prototype.intersects = function(rect) {
+  return goog.math.Rect.intersects(this, rect);
+};
+
+
+/**
+ * Computes the difference regions between two rectangles. The return value is
+ * an array of 0 to 4 rectangles defining the remaining regions of the first
+ * rectangle after the second has been subtracted.
+ * @param {goog.math.Rect} a A Rectangle.
+ * @param {goog.math.IRect} b A Rectangle.
+ * @return {!Array<!goog.math.Rect>} An array with 0 to 4 rectangles which
+ *     together define the difference area of rectangle a minus rectangle b.
+ */
+goog.math.Rect.difference = function(a, b) {
+  var intersection = goog.math.Rect.intersection(a, b);
+  if (!intersection || !intersection.height || !intersection.width) {
+    return [a.clone()];
+  }
+
+  var result = [];
+
+  var top = a.top;
+  var height = a.height;
+
+  var ar = a.left + a.width;
+  var ab = a.top + a.height;
+
+  var br = b.left + b.width;
+  var bb = b.top + b.height;
+
+  // Subtract off any area on top where A extends past B
+  if (b.top > a.top) {
+    result.push(new goog.math.Rect(a.left, a.top, a.width, b.top - a.top));
+    top = b.top;
+    // If we're moving the top down, we also need to subtract the height diff.
+    height -= b.top - a.top;
+  }
+  // Subtract off any area on bottom where A extends past B
+  if (bb < ab) {
+    result.push(new goog.math.Rect(a.left, bb, a.width, ab - bb));
+    height = bb - top;
+  }
+  // Subtract any area on left where A extends past B
+  if (b.left > a.left) {
+    result.push(new goog.math.Rect(a.left, top, b.left - a.left, height));
+  }
+  // Subtract any area on right where A extends past B
+  if (br < ar) {
+    result.push(new goog.math.Rect(br, top, ar - br, height));
+  }
+
+  return result;
+};
+
+
+/**
+ * Computes the difference regions between this rectangle and {@code rect}. The
+ * return value is an array of 0 to 4 rectangles defining the remaining regions
+ * of this rectangle after the other has been subtracted.
+ * @param {goog.math.IRect} rect A Rectangle.
+ * @return {!Array<!goog.math.Rect>} An array with 0 to 4 rectangles which
+ *     together define the difference area of rectangle a minus rectangle b.
+ */
+goog.math.Rect.prototype.difference = function(rect) {
+  return goog.math.Rect.difference(this, rect);
+};
+
+
+/**
+ * Expand this rectangle to also include the area of the given rectangle.
+ * @param {goog.math.IRect} rect The other rectangle.
+ */
+goog.math.Rect.prototype.boundingRect = function(rect) {
+  // We compute right and bottom before we change left and top below.
+  var right = Math.max(this.left + this.width, rect.left + rect.width);
+  var bottom = Math.max(this.top + this.height, rect.top + rect.height);
+
+  this.left = Math.min(this.left, rect.left);
+  this.top = Math.min(this.top, rect.top);
+
+  this.width = right - this.left;
+  this.height = bottom - this.top;
+};
+
+
+/**
+ * Returns a new rectangle which completely contains both input rectangles.
+ * @param {goog.math.IRect} a A rectangle.
+ * @param {goog.math.IRect} b A rectangle.
+ * @return {goog.math.Rect} A new bounding rect, or null if either rect is
+ *     null.
+ */
+goog.math.Rect.boundingRect = function(a, b) {
+  if (!a || !b) {
+    return null;
+  }
+
+  var newRect = new goog.math.Rect(a.left, a.top, a.width, a.height);
+  newRect.boundingRect(b);
+
+  return newRect;
+};
+
+
+/**
+ * Tests whether this rectangle entirely contains another rectangle or
+ * coordinate.
+ *
+ * @param {goog.math.IRect|goog.math.Coordinate} another The rectangle or
+ *     coordinate to test for containment.
+ * @return {boolean} Whether this rectangle contains given rectangle or
+ *     coordinate.
+ */
+goog.math.Rect.prototype.contains = function(another) {
+  if (another instanceof goog.math.Coordinate) {
+    return another.x >= this.left && another.x <= this.left + this.width &&
+        another.y >= this.top && another.y <= this.top + this.height;
+  } else {  // (another instanceof goog.math.IRect)
+    return this.left <= another.left &&
+        this.left + this.width >= another.left + another.width &&
+        this.top <= another.top &&
+        this.top + this.height >= another.top + another.height;
+  }
+};
+
+
+/**
+ * @param {!goog.math.Coordinate} point A coordinate.
+ * @return {number} The squared distance between the point and the closest
+ *     point inside the rectangle. Returns 0 if the point is inside the
+ *     rectangle.
+ */
+goog.math.Rect.prototype.squaredDistance = function(point) {
+  var dx = point.x < this.left ?
+      this.left - point.x :
+      Math.max(point.x - (this.left + this.width), 0);
+  var dy = point.y < this.top ? this.top - point.y :
+                                Math.max(point.y - (this.top + this.height), 0);
+  return dx * dx + dy * dy;
+};
+
+
+/**
+ * @param {!goog.math.Coordinate} point A coordinate.
+ * @return {number} The distance between the point and the closest point
+ *     inside the rectangle. Returns 0 if the point is inside the rectangle.
+ */
+goog.math.Rect.prototype.distance = function(point) {
+  return Math.sqrt(this.squaredDistance(point));
+};
+
+
+/**
+ * @return {!goog.math.Size} The size of this rectangle.
+ */
+goog.math.Rect.prototype.getSize = function() {
+  return new goog.math.Size(this.width, this.height);
+};
+
+
+/**
+ * @return {!goog.math.Coordinate} A new coordinate for the top-left corner of
+ *     the rectangle.
+ */
+goog.math.Rect.prototype.getTopLeft = function() {
+  return new goog.math.Coordinate(this.left, this.top);
+};
+
+
+/**
+ * @return {!goog.math.Coordinate} A new coordinate for the center of the
+ *     rectangle.
+ */
+goog.math.Rect.prototype.getCenter = function() {
+  return new goog.math.Coordinate(
+      this.left + this.width / 2, this.top + this.height / 2);
+};
+
+
+/**
+ * @return {!goog.math.Coordinate} A new coordinate for the bottom-right corner
+ *     of the rectangle.
+ */
+goog.math.Rect.prototype.getBottomRight = function() {
+  return new goog.math.Coordinate(
+      this.left + this.width, this.top + this.height);
+};
+
+
+/**
+ * Rounds the fields to the next larger integer values.
+ * @return {!goog.math.Rect} This rectangle with ceil'd fields.
+ */
+goog.math.Rect.prototype.ceil = function() {
+  this.left = Math.ceil(this.left);
+  this.top = Math.ceil(this.top);
+  this.width = Math.ceil(this.width);
+  this.height = Math.ceil(this.height);
+  return this;
+};
+
+
+/**
+ * Rounds the fields to the next smaller integer values.
+ * @return {!goog.math.Rect} This rectangle with floored fields.
+ */
+goog.math.Rect.prototype.floor = function() {
+  this.left = Math.floor(this.left);
+  this.top = Math.floor(this.top);
+  this.width = Math.floor(this.width);
+  this.height = Math.floor(this.height);
+  return this;
+};
+
+
+/**
+ * Rounds the fields to nearest integer values.
+ * @return {!goog.math.Rect} This rectangle with rounded fields.
+ */
+goog.math.Rect.prototype.round = function() {
+  this.left = Math.round(this.left);
+  this.top = Math.round(this.top);
+  this.width = Math.round(this.width);
+  this.height = Math.round(this.height);
+  return this;
+};
+
+
+/**
+ * Translates this rectangle by the given offsets. If a
+ * {@code goog.math.Coordinate} is given, then the left and top values are
+ * translated by the coordinate's x and y values. Otherwise, top and left are
+ * translated by {@code tx} and {@code opt_ty} respectively.
+ * @param {number|goog.math.Coordinate} tx The value to translate left by or the
+ *     the coordinate to translate this rect by.
+ * @param {number=} opt_ty The value to translate top by.
+ * @return {!goog.math.Rect} This rectangle after translating.
+ */
+goog.math.Rect.prototype.translate = function(tx, opt_ty) {
+  if (tx instanceof goog.math.Coordinate) {
+    this.left += tx.x;
+    this.top += tx.y;
+  } else {
+    this.left += goog.asserts.assertNumber(tx);
+    if (goog.isNumber(opt_ty)) {
+      this.top += opt_ty;
+    }
+  }
+  return this;
+};
+
+
+/**
+ * Scales this rectangle by the given scale factors. The left and width values
+ * are scaled by {@code sx} and the top and height values are scaled by
+ * {@code opt_sy}.  If {@code opt_sy} is not given, then all fields are scaled
+ * by {@code sx}.
+ * @param {number} sx The scale factor to use for the x dimension.
+ * @param {number=} opt_sy The scale factor to use for the y dimension.
+ * @return {!goog.math.Rect} This rectangle after scaling.
+ */
+goog.math.Rect.prototype.scale = function(sx, opt_sy) {
+  var sy = goog.isNumber(opt_sy) ? opt_sy : sx;
+  this.left *= sx;
+  this.width *= sx;
+  this.top *= sy;
+  this.height *= sy;
+  return this;
+};
diff --git a/third_party/ink/closure/math/size.js b/third_party/ink/closure/math/size.js
new file mode 100644
index 0000000..b539d0c
--- /dev/null
+++ b/third_party/ink/closure/math/size.js
@@ -0,0 +1,228 @@
+// Copyright 2007 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 A utility class for representing two-dimensional sizes.
+ * @author pupius@google.com (Dan Pupius)
+ * @author brenneman@google.com (Shawn Brenneman)
+ */
+
+
+goog.provide('goog.math.Size');
+
+
+
+/**
+ * Class for representing sizes consisting of a width and height. Undefined
+ * width and height support is deprecated and results in compiler warning.
+ * @param {number} width Width.
+ * @param {number} height Height.
+ * @struct
+ * @constructor
+ */
+goog.math.Size = function(width, height) {
+  /**
+   * Width
+   * @type {number}
+   */
+  this.width = width;
+
+  /**
+   * Height
+   * @type {number}
+   */
+  this.height = height;
+};
+
+
+/**
+ * Compares sizes for equality.
+ * @param {goog.math.Size} a A Size.
+ * @param {goog.math.Size} b A Size.
+ * @return {boolean} True iff the sizes have equal widths and equal
+ *     heights, or if both are null.
+ */
+goog.math.Size.equals = function(a, b) {
+  if (a == b) {
+    return true;
+  }
+  if (!a || !b) {
+    return false;
+  }
+  return a.width == b.width && a.height == b.height;
+};
+
+
+/**
+ * @return {!goog.math.Size} A new copy of the Size.
+ */
+goog.math.Size.prototype.clone = function() {
+  return new goog.math.Size(this.width, this.height);
+};
+
+
+if (goog.DEBUG) {
+  /**
+   * Returns a nice string representing size.
+   * @return {string} In the form (50 x 73).
+   * @override
+   */
+  goog.math.Size.prototype.toString = function() {
+    return '(' + this.width + ' x ' + this.height + ')';
+  };
+}
+
+
+/**
+ * @return {number} The longer of the two dimensions in the size.
+ */
+goog.math.Size.prototype.getLongest = function() {
+  return Math.max(this.width, this.height);
+};
+
+
+/**
+ * @return {number} The shorter of the two dimensions in the size.
+ */
+goog.math.Size.prototype.getShortest = function() {
+  return Math.min(this.width, this.height);
+};
+
+
+/**
+ * @return {number} The area of the size (width * height).
+ */
+goog.math.Size.prototype.area = function() {
+  return this.width * this.height;
+};
+
+
+/**
+ * @return {number} The perimeter of the size (width + height) * 2.
+ */
+goog.math.Size.prototype.perimeter = function() {
+  return (this.width + this.height) * 2;
+};
+
+
+/**
+ * @return {number} The ratio of the size's width to its height.
+ */
+goog.math.Size.prototype.aspectRatio = function() {
+  return this.width / this.height;
+};
+
+
+/**
+ * @return {boolean} True if the size has zero area, false if both dimensions
+ *     are non-zero numbers.
+ */
+goog.math.Size.prototype.isEmpty = function() {
+  return !this.area();
+};
+
+
+/**
+ * Clamps the width and height parameters upward to integer values.
+ * @return {!goog.math.Size} This size with ceil'd components.
+ */
+goog.math.Size.prototype.ceil = function() {
+  this.width = Math.ceil(this.width);
+  this.height = Math.ceil(this.height);
+  return this;
+};
+
+
+/**
+ * @param {!goog.math.Size} target The target size.
+ * @return {boolean} True if this Size is the same size or smaller than the
+ *     target size in both dimensions.
+ */
+goog.math.Size.prototype.fitsInside = function(target) {
+  return this.width <= target.width && this.height <= target.height;
+};
+
+
+/**
+ * Clamps the width and height parameters downward to integer values.
+ * @return {!goog.math.Size} This size with floored components.
+ */
+goog.math.Size.prototype.floor = function() {
+  this.width = Math.floor(this.width);
+  this.height = Math.floor(this.height);
+  return this;
+};
+
+
+/**
+ * Rounds the width and height parameters to integer values.
+ * @return {!goog.math.Size} This size with rounded components.
+ */
+goog.math.Size.prototype.round = function() {
+  this.width = Math.round(this.width);
+  this.height = Math.round(this.height);
+  return this;
+};
+
+
+/**
+ * Scales this size by the given scale factors. The width and height are scaled
+ * by {@code sx} and {@code opt_sy} respectively.  If {@code opt_sy} is not
+ * given, then {@code sx} is used for both the width and height.
+ * @param {number} sx The scale factor to use for the width.
+ * @param {number=} opt_sy The scale factor to use for the height.
+ * @return {!goog.math.Size} This Size object after scaling.
+ */
+goog.math.Size.prototype.scale = function(sx, opt_sy) {
+  var sy = goog.isNumber(opt_sy) ? opt_sy : sx;
+  this.width *= sx;
+  this.height *= sy;
+  return this;
+};
+
+
+/**
+ * Uniformly scales the size to perfectly cover the dimensions of a given size.
+ * If the size is already larger than the target, it will be scaled down to the
+ * minimum size at which it still covers the entire target. The original aspect
+ * ratio will be preserved.
+ *
+ * This function assumes that both Sizes contain strictly positive dimensions.
+ * @param {!goog.math.Size} target The target size.
+ * @return {!goog.math.Size} This Size object, after optional scaling.
+ */
+goog.math.Size.prototype.scaleToCover = function(target) {
+  var s = this.aspectRatio() <= target.aspectRatio() ?
+      target.width / this.width :
+      target.height / this.height;
+
+  return this.scale(s);
+};
+
+
+/**
+ * Uniformly scales the size to fit inside the dimensions of a given size. The
+ * original aspect ratio will be preserved.
+ *
+ * This function assumes that both Sizes contain strictly positive dimensions.
+ * @param {!goog.math.Size} target The target size.
+ * @return {!goog.math.Size} This Size object, after optional scaling.
+ */
+goog.math.Size.prototype.scaleToFit = function(target) {
+  var s = this.aspectRatio() > target.aspectRatio() ?
+      target.width / this.width :
+      target.height / this.height;
+
+  return this.scale(s);
+};
diff --git a/third_party/ink/closure/object/object.js b/third_party/ink/closure/object/object.js
new file mode 100644
index 0000000..286d24e
--- /dev/null
+++ b/third_party/ink/closure/object/object.js
@@ -0,0 +1,751 @@
+// Copyright 2006 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 Utilities for manipulating objects/maps/hashes.
+ * @author pupius@google.com (Daniel Pupius)
+ * @author arv@google.com (Erik Arvidsson)
+ * @author pallosp@google.com (Peter Pallos)
+ */
+
+goog.provide('goog.object');
+
+
+/**
+ * Whether two values are not observably distinguishable. This
+ * correctly detects that 0 is not the same as -0 and two NaNs are
+ * practically equivalent.
+ *
+ * The implementation is as suggested by harmony:egal proposal.
+ *
+ * @param {*} v The first value to compare.
+ * @param {*} v2 The second value to compare.
+ * @return {boolean} Whether two values are not observably distinguishable.
+ * @see http://wiki.ecmascript.org/doku.php?id=harmony:egal
+ */
+goog.object.is = function(v, v2) {
+  if (v === v2) {
+    // 0 === -0, but they are not identical.
+    // We need the cast because the compiler requires that v2 is a
+    // number (although 1/v2 works with non-number). We cast to ? to
+    // stop the compiler from type-checking this statement.
+    return v !== 0 || 1 / v === 1 / /** @type {?} */ (v2);
+  }
+
+  // NaN is non-reflexive: NaN !== NaN, although they are identical.
+  return v !== v && v2 !== v2;
+};
+
+
+/**
+ * Calls a function for each element in an object/map/hash.
+ *
+ * @param {Object<K,V>} obj The object over which to iterate.
+ * @param {function(this:T,V,?,Object<K,V>):?} f The function to call
+ *     for every element. This function takes 3 arguments (the value, the
+ *     key and the object) and the return value is ignored.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
+ * @template T,K,V
+ */
+goog.object.forEach = function(obj, f, opt_obj) {
+  for (var key in obj) {
+    f.call(/** @type {?} */ (opt_obj), obj[key], key, obj);
+  }
+};
+
+
+/**
+ * Calls a function for each element in an object/map/hash. If that call returns
+ * true, adds the element to a new object.
+ *
+ * @param {Object<K,V>} obj The object over which to iterate.
+ * @param {function(this:T,V,?,Object<K,V>):boolean} f The function to call
+ *     for every element. This
+ *     function takes 3 arguments (the value, the key and the object)
+ *     and should return a boolean. If the return value is true the
+ *     element is added to the result object. If it is false the
+ *     element is not included.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
+ * @return {!Object<K,V>} a new object in which only elements that passed the
+ *     test are present.
+ * @template T,K,V
+ */
+goog.object.filter = function(obj, f, opt_obj) {
+  var res = {};
+  for (var key in obj) {
+    if (f.call(/** @type {?} */ (opt_obj), obj[key], key, obj)) {
+      res[key] = obj[key];
+    }
+  }
+  return res;
+};
+
+
+/**
+ * For every element in an object/map/hash calls a function and inserts the
+ * result into a new object.
+ *
+ * @param {Object<K,V>} obj The object over which to iterate.
+ * @param {function(this:T,V,?,Object<K,V>):R} f The function to call
+ *     for every element. This function
+ *     takes 3 arguments (the value, the key and the object)
+ *     and should return something. The result will be inserted
+ *     into a new object.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
+ * @return {!Object<K,R>} a new object with the results from f.
+ * @template T,K,V,R
+ */
+goog.object.map = function(obj, f, opt_obj) {
+  var res = {};
+  for (var key in obj) {
+    res[key] = f.call(/** @type {?} */ (opt_obj), obj[key], key, obj);
+  }
+  return res;
+};
+
+
+/**
+ * Calls a function for each element in an object/map/hash. If any
+ * call returns true, returns true (without checking the rest). If
+ * all calls return false, returns false.
+ *
+ * @param {Object<K,V>} obj The object to check.
+ * @param {function(this:T,V,?,Object<K,V>):boolean} f The function to
+ *     call for every element. This function
+ *     takes 3 arguments (the value, the key and the object) and should
+ *     return a boolean.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
+ * @return {boolean} true if any element passes the test.
+ * @template T,K,V
+ */
+goog.object.some = function(obj, f, opt_obj) {
+  for (var key in obj) {
+    if (f.call(/** @type {?} */ (opt_obj), obj[key], key, obj)) {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Calls a function for each element in an object/map/hash. If
+ * all calls return true, returns true. If any call returns false, returns
+ * false at this point and does not continue to check the remaining elements.
+ *
+ * @param {Object<K,V>} obj The object to check.
+ * @param {?function(this:T,V,?,Object<K,V>):boolean} f The function to
+ *     call for every element. This function
+ *     takes 3 arguments (the value, the key and the object) and should
+ *     return a boolean.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
+ * @return {boolean} false if any element fails the test.
+ * @template T,K,V
+ */
+goog.object.every = function(obj, f, opt_obj) {
+  for (var key in obj) {
+    if (!f.call(/** @type {?} */ (opt_obj), obj[key], key, obj)) {
+      return false;
+    }
+  }
+  return true;
+};
+
+
+/**
+ * Returns the number of key-value pairs in the object map.
+ *
+ * @param {Object} obj The object for which to get the number of key-value
+ *     pairs.
+ * @return {number} The number of key-value pairs in the object map.
+ */
+goog.object.getCount = function(obj) {
+  var rv = 0;
+  for (var key in obj) {
+    rv++;
+  }
+  return rv;
+};
+
+
+/**
+ * Returns one key from the object map, if any exists.
+ * For map literals the returned key will be the first one in most of the
+ * browsers (a know exception is Konqueror).
+ *
+ * @param {Object} obj The object to pick a key from.
+ * @return {string|undefined} The key or undefined if the object is empty.
+ */
+goog.object.getAnyKey = function(obj) {
+  for (var key in obj) {
+    return key;
+  }
+};
+
+
+/**
+ * Returns one value from the object map, if any exists.
+ * For map literals the returned value will be the first one in most of the
+ * browsers (a know exception is Konqueror).
+ *
+ * @param {Object<K,V>} obj The object to pick a value from.
+ * @return {V|undefined} The value or undefined if the object is empty.
+ * @template K,V
+ */
+goog.object.getAnyValue = function(obj) {
+  for (var key in obj) {
+    return obj[key];
+  }
+};
+
+
+/**
+ * Whether the object/hash/map contains the given object as a value.
+ * An alias for goog.object.containsValue(obj, val).
+ *
+ * @param {Object<K,V>} obj The object in which to look for val.
+ * @param {V} val The object for which to check.
+ * @return {boolean} true if val is present.
+ * @template K,V
+ */
+goog.object.contains = function(obj, val) {
+  return goog.object.containsValue(obj, val);
+};
+
+
+/**
+ * Returns the values of the object/map/hash.
+ *
+ * @param {Object<K,V>} obj The object from which to get the values.
+ * @return {!Array<V>} The values in the object/map/hash.
+ * @template K,V
+ */
+goog.object.getValues = function(obj) {
+  var res = [];
+  var i = 0;
+  for (var key in obj) {
+    res[i++] = obj[key];
+  }
+  return res;
+};
+
+
+/**
+ * Returns the keys of the object/map/hash.
+ *
+ * @param {Object} obj The object from which to get the keys.
+ * @return {!Array<string>} Array of property keys.
+ */
+goog.object.getKeys = function(obj) {
+  var res = [];
+  var i = 0;
+  for (var key in obj) {
+    res[i++] = key;
+  }
+  return res;
+};
+
+
+/**
+ * Get a value from an object multiple levels deep.  This is useful for
+ * pulling values from deeply nested objects, such as JSON responses.
+ * Example usage: getValueByKeys(jsonObj, 'foo', 'entries', 3)
+ *
+ * @param {!Object} obj An object to get the value from.  Can be array-like.
+ * @param {...(string|number|!IArrayLike<number|string>)}
+ *     var_args A number of keys
+ *     (as strings, or numbers, for array-like objects).  Can also be
+ *     specified as a single array of keys.
+ * @return {*} The resulting value.  If, at any point, the value for a key
+ *     in the current object is null or undefined, returns undefined.
+ */
+goog.object.getValueByKeys = function(obj, var_args) {
+  var isArrayLike = goog.isArrayLike(var_args);
+  var keys = isArrayLike ? var_args : arguments;
+
+  // Start with the 2nd parameter for the variable parameters syntax.
+  for (var i = isArrayLike ? 0 : 1; i < keys.length; i++) {
+    if (obj == null) return undefined;
+    obj = obj[keys[i]];
+  }
+
+  return obj;
+};
+
+
+/**
+ * Whether the object/map/hash contains the given key.
+ *
+ * @param {Object} obj The object in which to look for key.
+ * @param {?} key The key for which to check.
+ * @return {boolean} true If the map contains the key.
+ */
+goog.object.containsKey = function(obj, key) {
+  return obj !== null && key in obj;
+};
+
+
+/**
+ * Whether the object/map/hash contains the given value. This is O(n).
+ *
+ * @param {Object<K,V>} obj The object in which to look for val.
+ * @param {V} val The value for which to check.
+ * @return {boolean} true If the map contains the value.
+ * @template K,V
+ */
+goog.object.containsValue = function(obj, val) {
+  for (var key in obj) {
+    if (obj[key] == val) {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Searches an object for an element that satisfies the given condition and
+ * returns its key.
+ * @param {Object<K,V>} obj The object to search in.
+ * @param {function(this:T,V,string,Object<K,V>):boolean} f The
+ *      function to call for every element. Takes 3 arguments (the value,
+ *     the key and the object) and should return a boolean.
+ * @param {T=} opt_this An optional "this" context for the function.
+ * @return {string|undefined} The key of an element for which the function
+ *     returns true or undefined if no such element is found.
+ * @template T,K,V
+ */
+goog.object.findKey = function(obj, f, opt_this) {
+  for (var key in obj) {
+    if (f.call(/** @type {?} */ (opt_this), obj[key], key, obj)) {
+      return key;
+    }
+  }
+  return undefined;
+};
+
+
+/**
+ * Searches an object for an element that satisfies the given condition and
+ * returns its value.
+ * @param {Object<K,V>} obj The object to search in.
+ * @param {function(this:T,V,string,Object<K,V>):boolean} f The function
+ *     to call for every element. Takes 3 arguments (the value, the key
+ *     and the object) and should return a boolean.
+ * @param {T=} opt_this An optional "this" context for the function.
+ * @return {V} The value of an element for which the function returns true or
+ *     undefined if no such element is found.
+ * @template T,K,V
+ */
+goog.object.findValue = function(obj, f, opt_this) {
+  var key = goog.object.findKey(obj, f, opt_this);
+  return key && obj[key];
+};
+
+
+/**
+ * Whether the object/map/hash is empty.
+ *
+ * @param {Object} obj The object to test.
+ * @return {boolean} true if obj is empty.
+ */
+goog.object.isEmpty = function(obj) {
+  for (var key in obj) {
+    return false;
+  }
+  return true;
+};
+
+
+/**
+ * Removes all key value pairs from the object/map/hash.
+ *
+ * @param {Object} obj The object to clear.
+ */
+goog.object.clear = function(obj) {
+  for (var i in obj) {
+    delete obj[i];
+  }
+};
+
+
+/**
+ * Removes a key-value pair based on the key.
+ *
+ * @param {Object} obj The object from which to remove the key.
+ * @param {?} key The key to remove.
+ * @return {boolean} Whether an element was removed.
+ */
+goog.object.remove = function(obj, key) {
+  var rv;
+  if (rv = key in /** @type {!Object} */ (obj)) {
+    delete obj[key];
+  }
+  return rv;
+};
+
+
+/**
+ * Adds a key-value pair to the object. Throws an exception if the key is
+ * already in use. Use set if you want to change an existing pair.
+ *
+ * @param {Object<K,V>} obj The object to which to add the key-value pair.
+ * @param {string} key The key to add.
+ * @param {V} val The value to add.
+ * @template K,V
+ */
+goog.object.add = function(obj, key, val) {
+  if (obj !== null && key in obj) {
+    throw new Error('The object already contains the key "' + key + '"');
+  }
+  goog.object.set(obj, key, val);
+};
+
+
+/**
+ * Returns the value for the given key.
+ *
+ * @param {Object<K,V>} obj The object from which to get the value.
+ * @param {string} key The key for which to get the value.
+ * @param {R=} opt_val The value to return if no item is found for the given
+ *     key (default is undefined).
+ * @return {V|R|undefined} The value for the given key.
+ * @template K,V,R
+ */
+goog.object.get = function(obj, key, opt_val) {
+  if (obj !== null && key in obj) {
+    return obj[key];
+  }
+  return opt_val;
+};
+
+
+/**
+ * Adds a key-value pair to the object/map/hash.
+ *
+ * @param {Object<K,V>} obj The object to which to add the key-value pair.
+ * @param {string} key The key to add.
+ * @param {V} value The value to add.
+ * @template K,V
+ */
+goog.object.set = function(obj, key, value) {
+  obj[key] = value;
+};
+
+
+/**
+ * Adds a key-value pair to the object/map/hash if it doesn't exist yet.
+ *
+ * @param {Object<K,V>} obj The object to which to add the key-value pair.
+ * @param {string} key The key to add.
+ * @param {V} value The value to add if the key wasn't present.
+ * @return {V} The value of the entry at the end of the function.
+ * @template K,V
+ */
+goog.object.setIfUndefined = function(obj, key, value) {
+  return key in /** @type {!Object} */ (obj) ? obj[key] : (obj[key] = value);
+};
+
+
+/**
+ * Sets a key and value to an object if the key is not set. The value will be
+ * the return value of the given function. If the key already exists, the
+ * object will not be changed and the function will not be called (the function
+ * will be lazily evaluated -- only called if necessary).
+ *
+ * This function is particularly useful for use with a map used a as a cache.
+ *
+ * @param {!Object<K,V>} obj The object to which to add the key-value pair.
+ * @param {string} key The key to add.
+ * @param {function():V} f The value to add if the key wasn't present.
+ * @return {V} The value of the entry at the end of the function.
+ * @template K,V
+ */
+goog.object.setWithReturnValueIfNotSet = function(obj, key, f) {
+  if (key in obj) {
+    return obj[key];
+  }
+
+  var val = f();
+  obj[key] = val;
+  return val;
+};
+
+
+/**
+ * Compares two objects for equality using === on the values.
+ *
+ * @param {!Object<K,V>} a
+ * @param {!Object<K,V>} b
+ * @return {boolean}
+ * @template K,V
+ */
+goog.object.equals = function(a, b) {
+  for (var k in a) {
+    if (!(k in b) || a[k] !== b[k]) {
+      return false;
+    }
+  }
+  for (var k in b) {
+    if (!(k in a)) {
+      return false;
+    }
+  }
+  return true;
+};
+
+
+/**
+ * Returns a shallow clone of the object.
+ *
+ * @param {Object<K,V>} obj Object to clone.
+ * @return {!Object<K,V>} Clone of the input object.
+ * @template K,V
+ */
+goog.object.clone = function(obj) {
+  // We cannot use the prototype trick because a lot of methods depend on where
+  // the actual key is set.
+
+  var res = {};
+  for (var key in obj) {
+    res[key] = obj[key];
+  }
+  return res;
+  // We could also use goog.mixin but I wanted this to be independent from that.
+};
+
+
+/**
+ * Clones a value. The input may be an Object, Array, or basic type. Objects and
+ * arrays will be cloned recursively.
+ *
+ * WARNINGS:
+ * <code>goog.object.unsafeClone</code> does not detect reference loops. Objects
+ * that refer to themselves will cause infinite recursion.
+ *
+ * <code>goog.object.unsafeClone</code> is unaware of unique identifiers, and
+ * copies UIDs created by <code>getUid</code> into cloned results.
+ *
+ * @param {T} obj The value to clone.
+ * @return {T} A clone of the input value.
+ * @template T
+ */
+goog.object.unsafeClone = function(obj) {
+  var type = goog.typeOf(obj);
+  if (type == 'object' || type == 'array') {
+    if (goog.isFunction(obj.clone)) {
+      return obj.clone();
+    }
+    var clone = type == 'array' ? [] : {};
+    for (var key in obj) {
+      clone[key] = goog.object.unsafeClone(obj[key]);
+    }
+    return clone;
+  }
+
+  return obj;
+};
+
+
+/**
+ * Returns a new object in which all the keys and values are interchanged
+ * (keys become values and values become keys). If multiple keys map to the
+ * same value, the chosen transposed value is implementation-dependent.
+ *
+ * @param {Object} obj The object to transpose.
+ * @return {!Object} The transposed object.
+ */
+goog.object.transpose = function(obj) {
+  var transposed = {};
+  for (var key in obj) {
+    transposed[obj[key]] = key;
+  }
+  return transposed;
+};
+
+
+/**
+ * The names of the fields that are defined on Object.prototype.
+ * @type {Array<string>}
+ * @private
+ */
+goog.object.PROTOTYPE_FIELDS_ = [
+  'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable',
+  'toLocaleString', 'toString', 'valueOf'
+];
+
+
+/**
+ * Extends an object with another object.
+ * This operates 'in-place'; it does not create a new Object.
+ *
+ * Example:
+ * var o = {};
+ * goog.object.extend(o, {a: 0, b: 1});
+ * o; // {a: 0, b: 1}
+ * goog.object.extend(o, {b: 2, c: 3});
+ * o; // {a: 0, b: 2, c: 3}
+ *
+ * @param {Object} target The object to modify. Existing properties will be
+ *     overwritten if they are also present in one of the objects in
+ *     {@code var_args}.
+ * @param {...Object} var_args The objects from which values will be copied.
+ */
+goog.object.extend = function(target, var_args) {
+  var key, source;
+  for (var i = 1; i < arguments.length; i++) {
+    source = arguments[i];
+    for (key in source) {
+      target[key] = source[key];
+    }
+
+    // For IE the for-in-loop does not contain any properties that are not
+    // enumerable on the prototype object (for example isPrototypeOf from
+    // Object.prototype) and it will also not include 'replace' on objects that
+    // extend String and change 'replace' (not that it is common for anyone to
+    // extend anything except Object).
+
+    for (var j = 0; j < goog.object.PROTOTYPE_FIELDS_.length; j++) {
+      key = goog.object.PROTOTYPE_FIELDS_[j];
+      if (Object.prototype.hasOwnProperty.call(source, key)) {
+        target[key] = source[key];
+      }
+    }
+  }
+};
+
+
+/**
+ * Creates a new object built from the key-value pairs provided as arguments.
+ * @param {...*} var_args If only one argument is provided and it is an array
+ *     then this is used as the arguments, otherwise even arguments are used as
+ *     the property names and odd arguments are used as the property values.
+ * @return {!Object} The new object.
+ * @throws {Error} If there are uneven number of arguments or there is only one
+ *     non array argument.
+ */
+goog.object.create = function(var_args) {
+  var argLength = arguments.length;
+  if (argLength == 1 && goog.isArray(arguments[0])) {
+    return goog.object.create.apply(null, arguments[0]);
+  }
+
+  if (argLength % 2) {
+    throw new Error('Uneven number of arguments');
+  }
+
+  var rv = {};
+  for (var i = 0; i < argLength; i += 2) {
+    rv[arguments[i]] = arguments[i + 1];
+  }
+  return rv;
+};
+
+
+/**
+ * Creates a new object where the property names come from the arguments but
+ * the value is always set to true
+ * @param {...*} var_args If only one argument is provided and it is an array
+ *     then this is used as the arguments, otherwise the arguments are used
+ *     as the property names.
+ * @return {!Object} The new object.
+ */
+goog.object.createSet = function(var_args) {
+  var argLength = arguments.length;
+  if (argLength == 1 && goog.isArray(arguments[0])) {
+    return goog.object.createSet.apply(null, arguments[0]);
+  }
+
+  var rv = {};
+  for (var i = 0; i < argLength; i++) {
+    rv[arguments[i]] = true;
+  }
+  return rv;
+};
+
+
+/**
+ * Creates an immutable view of the underlying object, if the browser
+ * supports immutable objects.
+ *
+ * In default mode, writes to this view will fail silently. In strict mode,
+ * they will throw an error.
+ *
+ * @param {!Object<K,V>} obj An object.
+ * @return {!Object<K,V>} An immutable view of that object, or the
+ *     original object if this browser does not support immutables.
+ * @template K,V
+ */
+goog.object.createImmutableView = function(obj) {
+  var result = obj;
+  if (Object.isFrozen && !Object.isFrozen(obj)) {
+    result = Object.create(obj);
+    Object.freeze(result);
+  }
+  return result;
+};
+
+
+/**
+ * @param {!Object} obj An object.
+ * @return {boolean} Whether this is an immutable view of the object.
+ */
+goog.object.isImmutableView = function(obj) {
+  return !!Object.isFrozen && Object.isFrozen(obj);
+};
+
+
+/**
+ * Get all properties names on a given Object regardless of enumerability.
+ *
+ * <p> If the browser does not support {@code Object.getOwnPropertyNames} nor
+ * {@code Object.getPrototypeOf} then this is equivalent to using {@code
+ * goog.object.getKeys}
+ *
+ * @param {?Object} obj The object to get the properties of.
+ * @param {boolean=} opt_includeObjectPrototype Whether properties defined on
+ *     {@code Object.prototype} should be included in the result.
+ * @param {boolean=} opt_includeFunctionPrototype Whether properties defined on
+ *     {@code Function.prototype} should be included in the result.
+ * @return {!Array<string>}
+ * @public
+ */
+goog.object.getAllPropertyNames = function(
+    obj, opt_includeObjectPrototype, opt_includeFunctionPrototype) {
+  if (!obj) {
+    return [];
+  }
+
+  // Naively use a for..in loop to get the property names if the browser doesn't
+  // support any other APIs for getting it.
+  if (!Object.getOwnPropertyNames || !Object.getPrototypeOf) {
+    return goog.object.getKeys(obj);
+  }
+
+  var visitedSet = {};
+
+  // Traverse the prototype chain and add all properties to the visited set.
+  var proto = obj;
+  while (proto &&
+         (proto !== Object.prototype || !!opt_includeObjectPrototype) &&
+         (proto !== Function.prototype || !!opt_includeFunctionPrototype)) {
+    var names = Object.getOwnPropertyNames(proto);
+    for (var i = 0; i < names.length; i++) {
+      visitedSet[names[i]] = true;
+    }
+    proto = Object.getPrototypeOf(proto);
+  }
+
+  return goog.object.getKeys(visitedSet);
+};
diff --git a/third_party/ink/closure/proto2/descriptor.js b/third_party/ink/closure/proto2/descriptor.js
new file mode 100644
index 0000000..4abc3a35
--- /dev/null
+++ b/third_party/ink/closure/proto2/descriptor.js
@@ -0,0 +1,202 @@
+// Copyright 2008 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 (Message) Descriptor class.
+ * @author jschorr@google.com (Joseph Schorr)
+ */
+
+goog.provide('goog.proto2.Descriptor');
+goog.provide('goog.proto2.Metadata');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.object');
+goog.require('goog.string');
+
+
+/**
+ * @typedef {{name: (string|undefined),
+ *            fullName: (string|undefined),
+ *            containingType: (goog.proto2.Message|undefined)}}
+ */
+goog.proto2.Metadata;
+
+
+
+/**
+ * A class which describes a Protocol Buffer 2 Message.
+ *
+ * @param {function(new:goog.proto2.Message)} messageType Constructor for
+ *      the message class that this descriptor describes.
+ * @param {!goog.proto2.Metadata} metadata The metadata about the message that
+ *      will be used to construct this descriptor.
+ * @param {Array<!goog.proto2.FieldDescriptor>} fields The fields of the
+ *      message described by this descriptor.
+ *
+ * @constructor
+ * @final
+ */
+goog.proto2.Descriptor = function(messageType, metadata, fields) {
+
+  /**
+   * @type {function(new:goog.proto2.Message)}
+   * @private
+   */
+  this.messageType_ = messageType;
+
+  /**
+   * @type {?string}
+   * @private
+   */
+  this.name_ = metadata.name || null;
+
+  /**
+   * @type {?string}
+   * @private
+   */
+  this.fullName_ = metadata.fullName || null;
+
+  /**
+   * @type {goog.proto2.Message|undefined}
+   * @private
+   */
+  this.containingType_ = metadata.containingType;
+
+  /**
+   * The fields of the message described by this descriptor.
+   * @type {!Object<number, !goog.proto2.FieldDescriptor>}
+   * @private
+   */
+  this.fields_ = {};
+
+  for (var i = 0; i < fields.length; i++) {
+    var field = fields[i];
+    this.fields_[field.getTag()] = field;
+  }
+};
+
+
+/**
+ * Returns the name of the message, if any.
+ *
+ * @return {?string} The name.
+ */
+goog.proto2.Descriptor.prototype.getName = function() {
+  return this.name_;
+};
+
+
+/**
+ * Returns the full name of the message, if any.
+ *
+ * @return {?string} The name.
+ */
+goog.proto2.Descriptor.prototype.getFullName = function() {
+  return this.fullName_;
+};
+
+
+/**
+ * Returns the descriptor of the containing message type or null if none.
+ *
+ * @return {goog.proto2.Descriptor} The descriptor.
+ */
+goog.proto2.Descriptor.prototype.getContainingType = function() {
+  if (!this.containingType_) {
+    return null;
+  }
+
+  return this.containingType_.getDescriptor();
+};
+
+
+/**
+ * Returns the fields in the message described by this descriptor ordered by
+ * tag.
+ *
+ * @return {!Array<!goog.proto2.FieldDescriptor>} The array of field
+ *     descriptors.
+ */
+goog.proto2.Descriptor.prototype.getFields = function() {
+  /**
+   * @param {!goog.proto2.FieldDescriptor} fieldA First field.
+   * @param {!goog.proto2.FieldDescriptor} fieldB Second field.
+   * @return {number} Negative if fieldA's tag number is smaller, positive
+   *     if greater, zero if the same.
+   */
+  function tagComparator(fieldA, fieldB) {
+    return fieldA.getTag() - fieldB.getTag();
+  }
+
+  var fields = goog.object.getValues(this.fields_);
+  goog.array.sort(fields, tagComparator);
+
+  return fields;
+};
+
+
+/**
+ * Returns the fields in the message as a key/value map, where the key is
+ * the tag number of the field. DO NOT MODIFY THE RETURNED OBJECT. We return
+ * the actual, internal, fields map for performance reasons, and changing the
+ * map can result in undefined behavior of this library.
+ *
+ * @return {!Object<number, !goog.proto2.FieldDescriptor>} The field map.
+ */
+goog.proto2.Descriptor.prototype.getFieldsMap = function() {
+  return this.fields_;
+};
+
+
+/**
+ * Returns the field matching the given name, if any. Note that
+ * this method searches over the *original* name of the field,
+ * not the camelCase version.
+ *
+ * @param {string} name The field name for which to search.
+ *
+ * @return {goog.proto2.FieldDescriptor} The field found, if any.
+ */
+goog.proto2.Descriptor.prototype.findFieldByName = function(name) {
+  var valueFound = goog.object.findValue(
+      this.fields_,
+      function(field, key, obj) { return field.getName() == name; });
+
+  return /** @type {goog.proto2.FieldDescriptor} */ (valueFound) || null;
+};
+
+
+/**
+ * Returns the field matching the given tag number, if any.
+ *
+ * @param {number|string} tag The field tag number for which to search.
+ *
+ * @return {goog.proto2.FieldDescriptor} The field found, if any.
+ */
+goog.proto2.Descriptor.prototype.findFieldByTag = function(tag) {
+  goog.asserts.assert(goog.string.isNumeric(tag));
+  return this.fields_[parseInt(tag, 10)] || null;
+};
+
+
+/**
+ * Creates an instance of the message type that this descriptor
+ * describes.
+ *
+ * @return {!goog.proto2.Message} The instance of the message.
+ */
+goog.proto2.Descriptor.prototype.createMessageInstance = function() {
+  return new this.messageType_;
+};
diff --git a/third_party/ink/closure/proto2/fielddescriptor.js b/third_party/ink/closure/proto2/fielddescriptor.js
new file mode 100644
index 0000000..7fbef98
--- /dev/null
+++ b/third_party/ink/closure/proto2/fielddescriptor.js
@@ -0,0 +1,313 @@
+// Copyright 2008 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 Field Descriptor class.
+ * @author jschorr@google.com (Joseph Schorr)
+ */
+
+goog.provide('goog.proto2.FieldDescriptor');
+
+goog.require('goog.asserts');
+goog.require('goog.string');
+
+
+
+/**
+ * A class which describes a field in a Protocol Buffer 2 Message.
+ *
+ * @param {function(new:goog.proto2.Message)} messageType Constructor for the
+ *     message class to which the field described by this class belongs.
+ * @param {number|string} tag The field's tag index.
+ * @param {Object} metadata The metadata about this field that will be used
+ *     to construct this descriptor.
+ *
+ * @constructor
+ * @final
+ */
+goog.proto2.FieldDescriptor = function(messageType, tag, metadata) {
+  /**
+   * The message type that contains the field that this
+   * descriptor describes.
+   * @private {function(new:goog.proto2.Message)}
+   */
+  this.parent_ = messageType;
+
+  // Ensure that the tag is numeric.
+  goog.asserts.assert(goog.string.isNumeric(tag));
+
+  /**
+   * The field's tag number.
+   * @private {number}
+   */
+  this.tag_ = /** @type {number} */ (tag);
+
+  /**
+   * The field's name.
+   * @private {string}
+   */
+  this.name_ = metadata.name;
+
+  /** @type {goog.proto2.FieldDescriptor.FieldType} */
+  metadata.fieldType;
+
+  /** @type {*} */
+  metadata.repeated;
+
+  /** @type {*} */
+  metadata.required;
+
+  /** @type {*} */
+  metadata.packed;
+
+  /**
+   * If true, this field is a packed field.
+   * @private {boolean}
+   */
+  this.isPacked_ = !!metadata.packed;
+
+  /**
+   * If true, this field is a repeating field.
+   * @private {boolean}
+   */
+  this.isRepeated_ = !!metadata.repeated;
+
+  /**
+   * If true, this field is required.
+   * @private {boolean}
+   */
+  this.isRequired_ = !!metadata.required;
+
+  /**
+   * The field type of this field.
+   * @private {goog.proto2.FieldDescriptor.FieldType}
+   */
+  this.fieldType_ = metadata.fieldType;
+
+  /**
+   * If this field is a primitive: The native (ECMAScript) type of this field.
+   * If an enumeration: The enumeration object.
+   * If a message or group field: The Message function.
+   * @private {Function}
+   */
+  this.nativeType_ = metadata.type;
+
+  /**
+   * Is it permissible on deserialization to convert between numbers and
+   * well-formed strings?  Is true for 64-bit integral field types and float and
+   * double types, false for all other field types.
+   * @private {boolean}
+   */
+  this.deserializationConversionPermitted_ = false;
+
+  switch (this.fieldType_) {
+    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:
+    case goog.proto2.FieldDescriptor.FieldType.FLOAT:
+    case goog.proto2.FieldDescriptor.FieldType.DOUBLE:
+      this.deserializationConversionPermitted_ = true;
+      break;
+  }
+
+  /**
+   * The default value of this field, if different from the default, default
+   * value.
+   * @private {*}
+   */
+  this.defaultValue_ = metadata.defaultValue;
+};
+
+
+/**
+ * An enumeration defining the possible field types.
+ * Should be a mirror of that defined in descriptor.h.
+ *
+ * @enum {number}
+ */
+goog.proto2.FieldDescriptor.FieldType = {
+  DOUBLE: 1,
+  FLOAT: 2,
+  INT64: 3,
+  UINT64: 4,
+  INT32: 5,
+  FIXED64: 6,
+  FIXED32: 7,
+  BOOL: 8,
+  STRING: 9,
+  GROUP: 10,
+  MESSAGE: 11,
+  BYTES: 12,
+  UINT32: 13,
+  ENUM: 14,
+  SFIXED32: 15,
+  SFIXED64: 16,
+  SINT32: 17,
+  SINT64: 18
+};
+
+
+/**
+ * Returns the tag of the field that this descriptor represents.
+ *
+ * @return {number} The tag number.
+ */
+goog.proto2.FieldDescriptor.prototype.getTag = function() {
+  return this.tag_;
+};
+
+
+/**
+ * Returns the descriptor describing the message that defined this field.
+ * @return {!goog.proto2.Descriptor} The descriptor.
+ */
+goog.proto2.FieldDescriptor.prototype.getContainingType = function() {
+  // Generated JS proto_library messages have getDescriptor() method which can
+  // be called with or without an instance.
+  return this.parent_.prototype.getDescriptor();
+};
+
+
+/**
+ * Returns the name of the field that this descriptor represents.
+ * @return {string} The name.
+ */
+goog.proto2.FieldDescriptor.prototype.getName = function() {
+  return this.name_;
+};
+
+
+/**
+ * Returns the default value of this field.
+ * @return {*} The default value.
+ */
+goog.proto2.FieldDescriptor.prototype.getDefaultValue = function() {
+  if (this.defaultValue_ === undefined) {
+    // Set the default value based on a new instance of the native type.
+    // This will be (0, false, "") for (number, boolean, string) and will
+    // be a new instance of a group/message if the field is a message type.
+    var nativeType = this.nativeType_;
+    if (nativeType === Boolean) {
+      this.defaultValue_ = false;
+    } else if (nativeType === Number) {
+      this.defaultValue_ = 0;
+    } else if (nativeType === String) {
+      if (this.deserializationConversionPermitted_) {
+        // This field is a 64 bit integer represented as a string.
+        this.defaultValue_ = '0';
+      } else {
+        this.defaultValue_ = '';
+      }
+    } else {
+      return new nativeType;
+    }
+  }
+
+  return this.defaultValue_;
+};
+
+
+/**
+ * Returns the field type of the field described by this descriptor.
+ * @return {goog.proto2.FieldDescriptor.FieldType} The field type.
+ */
+goog.proto2.FieldDescriptor.prototype.getFieldType = function() {
+  return this.fieldType_;
+};
+
+
+/**
+ * Returns the native (i.e. ECMAScript) type of the field described by this
+ * descriptor.
+ *
+ * @return {Object} The native type.
+ */
+goog.proto2.FieldDescriptor.prototype.getNativeType = function() {
+  return this.nativeType_;
+};
+
+
+/**
+ * Returns true if simple conversions between numbers and strings are permitted
+ * during deserialization for this field.
+ *
+ * @return {boolean} Whether conversion is permitted.
+ */
+goog.proto2.FieldDescriptor.prototype.deserializationConversionPermitted =
+    function() {
+  return this.deserializationConversionPermitted_;
+};
+
+
+/**
+ * Returns the descriptor of the message type of this field. Only valid
+ * for fields of type GROUP and MESSAGE.
+ *
+ * @return {!goog.proto2.Descriptor} The message descriptor.
+ */
+goog.proto2.FieldDescriptor.prototype.getFieldMessageType = function() {
+  // Generated JS proto_library messages have getDescriptor() method which can
+  // be called with or without an instance.
+  var messageClass =
+      /** @type {function(new:goog.proto2.Message)} */ (this.nativeType_);
+  return messageClass.prototype.getDescriptor();
+};
+
+
+/**
+ * @return {boolean} True if the field stores composite data or repeated
+ *     composite data (message or group).
+ */
+goog.proto2.FieldDescriptor.prototype.isCompositeType = function() {
+  return this.fieldType_ == goog.proto2.FieldDescriptor.FieldType.MESSAGE ||
+      this.fieldType_ == goog.proto2.FieldDescriptor.FieldType.GROUP;
+};
+
+
+/**
+ * Returns whether the field described by this descriptor is packed.
+ * @return {boolean} Whether the field is packed.
+ */
+goog.proto2.FieldDescriptor.prototype.isPacked = function() {
+  return this.isPacked_;
+};
+
+
+/**
+ * Returns whether the field described by this descriptor is repeating.
+ * @return {boolean} Whether the field is repeated.
+ */
+goog.proto2.FieldDescriptor.prototype.isRepeated = function() {
+  return this.isRepeated_;
+};
+
+
+/**
+ * Returns whether the field described by this descriptor is required.
+ * @return {boolean} Whether the field is required.
+ */
+goog.proto2.FieldDescriptor.prototype.isRequired = function() {
+  return this.isRequired_;
+};
+
+
+/**
+ * Returns whether the field described by this descriptor is optional.
+ * @return {boolean} Whether the field is optional.
+ */
+goog.proto2.FieldDescriptor.prototype.isOptional = function() {
+  return !this.isRepeated_ && !this.isRequired_;
+};
diff --git a/third_party/ink/closure/proto2/message.js b/third_party/ink/closure/proto2/message.js
new file mode 100644
index 0000000..0315680
--- /dev/null
+++ b/third_party/ink/closure/proto2/message.js
@@ -0,0 +1,733 @@
+// Copyright 2008 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 Message base class.
+ * @author jschorr@google.com (Joseph Schorr)
+ * @author pallosp@google.com (Peter Pallos)
+ * @suppress {unusedPrivateMembers} For descriptor_ declaration.
+ */
+
+goog.provide('goog.proto2.Message');
+
+goog.require('goog.asserts');
+goog.require('goog.proto2.Descriptor');
+goog.require('goog.proto2.FieldDescriptor');
+
+goog.forwardDeclare('goog.proto2.LazyDeserializer');  // circular reference
+
+
+
+/**
+ * Abstract base class for all Protocol Buffer 2 messages. It will be
+ * subclassed in the code generated by the Protocol Compiler. Any other
+ * subclasses are prohibited.
+ * @constructor
+ */
+goog.proto2.Message = function() {
+  /**
+   * Stores the field values in this message. Keyed by the tag of the fields.
+   * @type {!Object}
+   * @private
+   */
+  this.values_ = {};
+
+  /**
+   * Stores the field information (i.e. metadata) about this message.
+   * @type {Object<number, !goog.proto2.FieldDescriptor>}
+   * @private
+   */
+  this.fields_ = this.getDescriptor().getFieldsMap();
+
+  /**
+   * The lazy deserializer for this message instance, if any.
+   * @type {goog.proto2.LazyDeserializer}
+   * @private
+   */
+  this.lazyDeserializer_ = null;
+
+  /**
+   * A map of those fields deserialized, from tag number to their deserialized
+   * value.
+   * @type {Object}
+   * @private
+   */
+  this.deserializedFields_ = null;
+};
+
+
+/**
+ * An enumeration defining the possible field types.
+ * Should be a mirror of that defined in descriptor.h.
+ *
+ * TODO(sra): Remove this alias.  The code generator generates code that
+ * references this enum, so it needs to exist until the code generator is
+ * changed.  The enum was moved to from Message to FieldDescriptor to avoid a
+ * dependency cycle.
+ *
+ * Use goog.proto2.FieldDescriptor.FieldType instead.
+ *
+ * @enum {number}
+ */
+goog.proto2.Message.FieldType = {
+  DOUBLE: 1,
+  FLOAT: 2,
+  INT64: 3,
+  UINT64: 4,
+  INT32: 5,
+  FIXED64: 6,
+  FIXED32: 7,
+  BOOL: 8,
+  STRING: 9,
+  GROUP: 10,
+  MESSAGE: 11,
+  BYTES: 12,
+  UINT32: 13,
+  ENUM: 14,
+  SFIXED32: 15,
+  SFIXED64: 16,
+  SINT32: 17,
+  SINT64: 18
+};
+
+
+/**
+ * All instances of goog.proto2.Message should have a static descriptor_
+ * property. The Descriptor will be deserialized lazily in the getDescriptor()
+ * method.
+ *
+ * This declaration is just here for documentation purposes.
+ * goog.proto2.Message does not have its own descriptor.
+ *
+ * @type {undefined}
+ * @private
+ */
+goog.proto2.Message.descriptor_;
+
+
+/**
+ * Initializes the message with a lazy deserializer and its associated data.
+ * This method should be called by internal methods ONLY.
+ *
+ * @param {goog.proto2.LazyDeserializer} deserializer The lazy deserializer to
+ *   use to decode the data on the fly.
+ *
+ * @param {?} data The data to decode/deserialize.
+ */
+goog.proto2.Message.prototype.initializeForLazyDeserializer = function(
+    deserializer, data) {
+
+  this.lazyDeserializer_ = deserializer;
+  this.values_ = data;
+  this.deserializedFields_ = {};
+};
+
+
+/**
+ * Sets the value of an unknown field, by tag.
+ *
+ * @param {number} tag The tag of an unknown field (must be >= 1).
+ * @param {*} value The value for that unknown field.
+ */
+goog.proto2.Message.prototype.setUnknown = function(tag, value) {
+  goog.asserts.assert(
+      !this.fields_[tag], 'Field is not unknown in this message');
+  goog.asserts.assert(
+      tag >= 1, 'Tag ' + tag + ' has value "' + value + '" in descriptor ' +
+          this.getDescriptor().getName());
+
+  goog.asserts.assert(value !== null, 'Value cannot be null');
+
+  this.values_[tag] = value;
+  if (this.deserializedFields_) {
+    delete this.deserializedFields_[tag];
+  }
+};
+
+
+/**
+ * Iterates over all the unknown fields in the message.
+ *
+ * @param {function(this:T, number, *)} callback A callback method
+ *     which gets invoked for each unknown field.
+ * @param {T=} opt_scope The scope under which to execute the callback.
+ *     If not given, the current message will be used.
+ * @template T
+ */
+goog.proto2.Message.prototype.forEachUnknown = function(callback, opt_scope) {
+  var scope = opt_scope || this;
+  for (var key in this.values_) {
+    var keyNum = Number(key);
+    if (!this.fields_[keyNum]) {
+      callback.call(scope, keyNum, this.values_[key]);
+    }
+  }
+};
+
+
+/**
+ * Returns the descriptor which describes the current message.
+ *
+ * This only works if we assume people never subclass protobufs.
+ *
+ * @return {!goog.proto2.Descriptor} The descriptor.
+ */
+goog.proto2.Message.prototype.getDescriptor = goog.abstractMethod;
+
+
+/**
+ * Returns whether there is a value stored at the field specified by the
+ * given field descriptor.
+ *
+ * @param {goog.proto2.FieldDescriptor} field The field for which to check
+ *     if there is a value.
+ *
+ * @return {boolean} True if a value was found.
+ */
+goog.proto2.Message.prototype.has = function(field) {
+  goog.asserts.assert(
+      field.getContainingType() == this.getDescriptor(),
+      'The current message does not contain the given field');
+
+  return this.has$Value(field.getTag());
+};
+
+
+/**
+ * Returns the array of values found for the given repeated field.
+ *
+ * @param {goog.proto2.FieldDescriptor} field The field for which to
+ *     return the values.
+ *
+ * @return {!Array<?>} The values found.
+ */
+goog.proto2.Message.prototype.arrayOf = function(field) {
+  goog.asserts.assert(
+      field.getContainingType() == this.getDescriptor(),
+      'The current message does not contain the given field');
+
+  return this.array$Values(field.getTag());
+};
+
+
+/**
+ * Returns the number of values stored in the given field.
+ *
+ * @param {goog.proto2.FieldDescriptor} field The field for which to count
+ *     the number of values.
+ *
+ * @return {number} The count of the values in the given field.
+ */
+goog.proto2.Message.prototype.countOf = function(field) {
+  goog.asserts.assert(
+      field.getContainingType() == this.getDescriptor(),
+      'The current message does not contain the given field');
+
+  return this.count$Values(field.getTag());
+};
+
+
+/**
+ * Returns the value stored at the field specified by the
+ * given field descriptor.
+ *
+ * @param {goog.proto2.FieldDescriptor} field The field for which to get the
+ *     value.
+ * @param {number=} opt_index If the field is repeated, the index to use when
+ *     looking up the value.
+ *
+ * @return {?} The value found or null if none.
+ */
+goog.proto2.Message.prototype.get = function(field, opt_index) {
+  goog.asserts.assert(
+      field.getContainingType() == this.getDescriptor(),
+      'The current message does not contain the given field');
+
+  return this.get$Value(field.getTag(), opt_index);
+};
+
+
+/**
+ * Returns the value stored at the field specified by the
+ * given field descriptor or the default value if none exists.
+ *
+ * @param {goog.proto2.FieldDescriptor} field The field for which to get the
+ *     value.
+ * @param {number=} opt_index If the field is repeated, the index to use when
+ *     looking up the value.
+ *
+ * @return {?} The value found or the default if none.
+ */
+goog.proto2.Message.prototype.getOrDefault = function(field, opt_index) {
+  goog.asserts.assert(
+      field.getContainingType() == this.getDescriptor(),
+      'The current message does not contain the given field');
+
+  return this.get$ValueOrDefault(field.getTag(), opt_index);
+};
+
+
+/**
+ * Stores the given value to the field specified by the
+ * given field descriptor. Note that the field must not be repeated.
+ *
+ * @param {goog.proto2.FieldDescriptor} field The field for which to set
+ *     the value.
+ * @param {*} value The new value for the field.
+ */
+goog.proto2.Message.prototype.set = function(field, value) {
+  goog.asserts.assert(
+      field.getContainingType() == this.getDescriptor(),
+      'The current message does not contain the given field');
+
+  this.set$Value(field.getTag(), value);
+};
+
+
+/**
+ * Adds the given value to the field specified by the
+ * given field descriptor. Note that the field must be repeated.
+ *
+ * @param {goog.proto2.FieldDescriptor} field The field in which to add the
+ *     the value.
+ * @param {*} value The new value to add to the field.
+ */
+goog.proto2.Message.prototype.add = function(field, value) {
+  goog.asserts.assert(
+      field.getContainingType() == this.getDescriptor(),
+      'The current message does not contain the given field');
+
+  this.add$Value(field.getTag(), value);
+};
+
+
+/**
+ * Clears the field specified.
+ *
+ * @param {goog.proto2.FieldDescriptor} field The field to clear.
+ */
+goog.proto2.Message.prototype.clear = function(field) {
+  goog.asserts.assert(
+      field.getContainingType() == this.getDescriptor(),
+      'The current message does not contain the given field');
+
+  this.clear$Field(field.getTag());
+};
+
+
+/**
+ * Compares this message with another one ignoring the unknown fields.
+ * @param {?} other The other message.
+ * @return {boolean} Whether they are equal. Returns false if the {@code other}
+ *     argument is a different type of message or not a message.
+ */
+goog.proto2.Message.prototype.equals = function(other) {
+  if (!other || this.constructor != other.constructor) {
+    return false;
+  }
+
+  var fields = this.getDescriptor().getFields();
+  for (var i = 0; i < fields.length; i++) {
+    var field = fields[i];
+    var tag = field.getTag();
+    if (this.has$Value(tag) != other.has$Value(tag)) {
+      return false;
+    }
+
+    if (this.has$Value(tag)) {
+      var isComposite = field.isCompositeType();
+
+      var fieldsEqual = function(value1, value2) {
+        return isComposite ? value1.equals(value2) : value1 == value2;
+      };
+
+      var thisValue = this.getValueForTag_(tag);
+      var otherValue = other.getValueForTag_(tag);
+
+      if (field.isRepeated()) {
+        // In this case thisValue and otherValue are arrays.
+        if (thisValue.length != otherValue.length) {
+          return false;
+        }
+        for (var j = 0; j < thisValue.length; j++) {
+          if (!fieldsEqual(thisValue[j], otherValue[j])) {
+            return false;
+          }
+        }
+      } else if (!fieldsEqual(thisValue, otherValue)) {
+        return false;
+      }
+    }
+  }
+
+  return true;
+};
+
+
+/**
+ * Recursively copies the known fields from the given message to this message.
+ * Removes the fields which are not present in the source message.
+ * @param {!goog.proto2.Message} message The source message.
+ */
+goog.proto2.Message.prototype.copyFrom = function(message) {
+  goog.asserts.assert(
+      this.constructor == message.constructor,
+      'The source message must have the same type.');
+
+  if (this != message) {
+    this.values_ = {};
+    if (this.deserializedFields_) {
+      this.deserializedFields_ = {};
+    }
+    this.mergeFrom(message);
+  }
+};
+
+
+/**
+ * Merges the given message into this message.
+ *
+ * Singular fields will be overwritten, except for embedded messages which will
+ * be merged. Repeated fields will be concatenated.
+ * @param {!goog.proto2.Message} message The source message.
+ */
+goog.proto2.Message.prototype.mergeFrom = function(message) {
+  goog.asserts.assert(
+      this.constructor == message.constructor,
+      'The source message must have the same type.');
+  var fields = this.getDescriptor().getFields();
+
+  for (var i = 0; i < fields.length; i++) {
+    var field = fields[i];
+    var tag = field.getTag();
+    if (message.has$Value(tag)) {
+      if (this.deserializedFields_) {
+        delete this.deserializedFields_[field.getTag()];
+      }
+
+      var isComposite = field.isCompositeType();
+      if (field.isRepeated()) {
+        var values = message.array$Values(tag);
+        for (var j = 0; j < values.length; j++) {
+          this.add$Value(tag, isComposite ? values[j].clone() : values[j]);
+        }
+      } else {
+        var value = message.getValueForTag_(tag);
+        if (isComposite) {
+          var child = this.getValueForTag_(tag);
+          if (child) {
+            child.mergeFrom(value);
+          } else {
+            this.set$Value(tag, value.clone());
+          }
+        } else {
+          this.set$Value(tag, value);
+        }
+      }
+    }
+  }
+};
+
+
+/**
+ * @return {!goog.proto2.Message} Recursive clone of the message only including
+ *     the known fields.
+ */
+goog.proto2.Message.prototype.clone = function() {
+  /** @type {!goog.proto2.Message} */
+  var clone = new this.constructor;
+  clone.copyFrom(this);
+  return clone;
+};
+
+
+/**
+ * Fills in the protocol buffer with default values. Any fields that are
+ * already set will not be overridden.
+ * @param {boolean} simpleFieldsToo If true, all fields will be initialized;
+ *     if false, only the nested messages and groups.
+ */
+goog.proto2.Message.prototype.initDefaults = function(simpleFieldsToo) {
+  var fields = this.getDescriptor().getFields();
+  for (var i = 0; i < fields.length; i++) {
+    var field = fields[i];
+    var tag = field.getTag();
+    var isComposite = field.isCompositeType();
+
+    // Initialize missing fields.
+    if (!this.has$Value(tag) && !field.isRepeated()) {
+      if (isComposite) {
+        this.values_[tag] = new /** @type {Function} */ (field.getNativeType());
+      } else if (simpleFieldsToo) {
+        this.values_[tag] = field.getDefaultValue();
+      }
+    }
+
+    // Fill in the existing composite fields recursively.
+    if (isComposite) {
+      if (field.isRepeated()) {
+        var values = this.array$Values(tag);
+        for (var j = 0; j < values.length; j++) {
+          values[j].initDefaults(simpleFieldsToo);
+        }
+      } else {
+        this.get$Value(tag).initDefaults(simpleFieldsToo);
+      }
+    }
+  }
+};
+
+
+/**
+ * Returns the whether or not the field indicated by the given tag
+ * has a value.
+ *
+ * GENERATED CODE USE ONLY. Basis of the has{Field} methods.
+ *
+ * @param {number} tag The tag.
+ *
+ * @return {boolean} Whether the message has a value for the field.
+ */
+goog.proto2.Message.prototype.has$Value = function(tag) {
+  return this.values_[tag] != null;
+};
+
+
+/**
+ * Returns the value for the given tag number. If a lazy deserializer is
+ * instantiated, lazily deserializes the field if required before returning the
+ * value.
+ *
+ * @param {number} tag The tag number.
+ * @return {?} The corresponding value, if any.
+ * @private
+ */
+goog.proto2.Message.prototype.getValueForTag_ = function(tag) {
+  // Retrieve the current value, which may still be serialized.
+  var value = this.values_[tag];
+  if (!goog.isDefAndNotNull(value)) {
+    return null;
+  }
+
+  // If we have a lazy deserializer, then ensure that the field is
+  // properly deserialized.
+  if (this.lazyDeserializer_) {
+    // If the tag is not deserialized, then we must do so now. Deserialize
+    // the field's value via the deserializer.
+    if (!(tag in /** @type {!Object} */ (this.deserializedFields_))) {
+      var deserializedValue = this.lazyDeserializer_.deserializeField(
+          this, this.fields_[tag], value);
+      this.deserializedFields_[tag] = deserializedValue;
+      return deserializedValue;
+    }
+
+    return this.deserializedFields_[tag];
+  }
+
+  // Otherwise, just return the value.
+  return value;
+};
+
+
+/**
+ * Gets the value at the field indicated by the given tag.
+ *
+ * GENERATED CODE USE ONLY. Basis of the get{Field} methods.
+ *
+ * @param {number} tag The field's tag index.
+ * @param {number=} opt_index If the field is a repeated field, the index
+ *     at which to get the value.
+ *
+ * @return {?} The value found or null for none.
+ * @protected
+ */
+goog.proto2.Message.prototype.get$Value = function(tag, opt_index) {
+  var value = this.getValueForTag_(tag);
+
+  if (this.fields_[tag].isRepeated()) {
+    var index = opt_index || 0;
+    goog.asserts.assert(
+        index >= 0 && index < value.length,
+        'Given index %s is out of bounds.  Repeated field length: %s', index,
+        value.length);
+    return value[index];
+  }
+
+  return value;
+};
+
+
+/**
+ * Gets the value at the field indicated by the given tag or the default value
+ * if none.
+ *
+ * GENERATED CODE USE ONLY. Basis of the get{Field} methods.
+ *
+ * @param {number} tag The field's tag index.
+ * @param {number=} opt_index If the field is a repeated field, the index
+ *     at which to get the value.
+ *
+ * @return {?} The value found or the default value if none set.
+ * @protected
+ */
+goog.proto2.Message.prototype.get$ValueOrDefault = function(tag, opt_index) {
+  if (!this.has$Value(tag)) {
+    // Return the default value.
+    var field = this.fields_[tag];
+    return field.getDefaultValue();
+  }
+
+  return this.get$Value(tag, opt_index);
+};
+
+
+/**
+ * Gets the values at the field indicated by the given tag.
+ *
+ * GENERATED CODE USE ONLY. Basis of the {field}Array methods.
+ *
+ * @param {number} tag The field's tag index.
+ *
+ * @return {!Array<?>} The values found. If none, returns an empty array.
+ * @protected
+ */
+goog.proto2.Message.prototype.array$Values = function(tag) {
+  var value = this.getValueForTag_(tag);
+  return value || [];
+};
+
+
+/**
+ * Returns the number of values stored in the field by the given tag.
+ *
+ * GENERATED CODE USE ONLY. Basis of the {field}Count methods.
+ *
+ * @param {number} tag The tag.
+ *
+ * @return {number} The number of values.
+ * @protected
+ */
+goog.proto2.Message.prototype.count$Values = function(tag) {
+  var field = this.fields_[tag];
+  if (field.isRepeated()) {
+    return this.has$Value(tag) ? this.values_[tag].length : 0;
+  } else {
+    return this.has$Value(tag) ? 1 : 0;
+  }
+};
+
+
+/**
+ * Sets the value of the *non-repeating* field indicated by the given tag.
+ *
+ * GENERATED CODE USE ONLY. Basis of the set{Field} methods.
+ *
+ * @param {number} tag The field's tag index.
+ * @param {*} value The field's value.
+ * @protected
+ */
+goog.proto2.Message.prototype.set$Value = function(tag, value) {
+  if (goog.asserts.ENABLE_ASSERTS) {
+    var field = this.fields_[tag];
+    this.checkFieldType_(field, value);
+  }
+
+  this.values_[tag] = value;
+  if (this.deserializedFields_) {
+    this.deserializedFields_[tag] = value;
+  }
+};
+
+
+/**
+ * Adds the value to the *repeating* field indicated by the given tag.
+ *
+ * GENERATED CODE USE ONLY. Basis of the add{Field} methods.
+ *
+ * @param {number} tag The field's tag index.
+ * @param {*} value The value to add.
+ * @protected
+ */
+goog.proto2.Message.prototype.add$Value = function(tag, value) {
+  if (goog.asserts.ENABLE_ASSERTS) {
+    var field = this.fields_[tag];
+    this.checkFieldType_(field, value);
+  }
+
+  if (!this.values_[tag]) {
+    this.values_[tag] = [];
+  }
+
+  this.values_[tag].push(value);
+  if (this.deserializedFields_) {
+    delete this.deserializedFields_[tag];
+  }
+};
+
+
+/**
+ * Ensures that the value being assigned to the given field
+ * is valid.
+ *
+ * @param {!goog.proto2.FieldDescriptor} field The field being assigned.
+ * @param {*} value The value being assigned.
+ * @private
+ */
+goog.proto2.Message.prototype.checkFieldType_ = function(field, value) {
+  if (field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.ENUM) {
+    goog.asserts.assertNumber(value);
+  } else {
+    goog.asserts.assert(Object(value).constructor == field.getNativeType());
+  }
+};
+
+
+/**
+ * Clears the field specified by tag.
+ *
+ * GENERATED CODE USE ONLY. Basis of the clear{Field} methods.
+ *
+ * @param {number} tag The tag of the field to clear.
+ * @protected
+ */
+goog.proto2.Message.prototype.clear$Field = function(tag) {
+  delete this.values_[tag];
+  if (this.deserializedFields_) {
+    delete this.deserializedFields_[tag];
+  }
+};
+
+
+/**
+ * Creates the metadata descriptor representing the definition of this message.
+ *
+ * @param {function(new:goog.proto2.Message)} messageType Constructor for the
+ *     message type to which this metadata applies.
+ * @param {!Object} metadataObj The object containing the metadata.
+ * @return {!goog.proto2.Descriptor} The new descriptor.
+ */
+goog.proto2.Message.createDescriptor = function(messageType, metadataObj) {
+  var fields = [];
+  var descriptorInfo = metadataObj[0];
+
+  for (var key in metadataObj) {
+    if (key != 0) {
+      // Create the field descriptor.
+      fields.push(
+          new goog.proto2.FieldDescriptor(messageType, key, metadataObj[key]));
+    }
+  }
+
+  return new goog.proto2.Descriptor(messageType, descriptorInfo, fields);
+};
diff --git a/third_party/ink/closure/proto2/objectserializer.js b/third_party/ink/closure/proto2/objectserializer.js
new file mode 100644
index 0000000..95fa5d3f
--- /dev/null
+++ b/third_party/ink/closure/proto2/objectserializer.js
@@ -0,0 +1,202 @@
+// Copyright 2008 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 anonymous, simplified JSON objects.
+ *
+ * @author jschorr@google.com (Joseph Schorr)
+ */
+
+goog.provide('goog.proto2.ObjectSerializer');
+
+goog.require('goog.asserts');
+goog.require('goog.proto2.FieldDescriptor');
+goog.require('goog.proto2.Serializer');
+goog.require('goog.string');
+
+
+
+/**
+ * ObjectSerializer, a serializer which turns Messages into simplified
+ * ECMAScript objects.
+ *
+ * @param {goog.proto2.ObjectSerializer.KeyOption=} opt_keyOption If specified,
+ *     which key option to use when serializing/deserializing.
+ * @param {boolean=} opt_serializeBooleanAsNumber If specified and true, the
+ *     serializer will convert boolean values to 0/1 representation.
+ * @constructor
+ * @extends {goog.proto2.Serializer}
+ */
+goog.proto2.ObjectSerializer = function(
+    opt_keyOption, opt_serializeBooleanAsNumber) {
+  this.keyOption_ = opt_keyOption;
+  this.serializeBooleanAsNumber_ = opt_serializeBooleanAsNumber;
+};
+goog.inherits(goog.proto2.ObjectSerializer, goog.proto2.Serializer);
+
+
+/**
+ * An enumeration of the options for how to emit the keys in
+ * the generated simplified object.
+ *
+ * @enum {number}
+ */
+goog.proto2.ObjectSerializer.KeyOption = {
+  /**
+   * Use the tag of the field as the key (default)
+   */
+  TAG: 0,
+
+  /**
+   * Use the name of the field as the key. Unknown fields
+   * will still use their tags as keys.
+   */
+  NAME: 1
+};
+
+
+/**
+ * Serializes a message to an object.
+ *
+ * @param {goog.proto2.Message} message The message to be serialized.
+ * @return {!Object} The serialized form of the message.
+ * @override
+ */
+goog.proto2.ObjectSerializer.prototype.serialize = function(message) {
+  var descriptor = message.getDescriptor();
+  var fields = descriptor.getFields();
+
+  var objectValue = {};
+
+  // Add the defined fields, recursively.
+  for (var i = 0; i < fields.length; i++) {
+    var field = fields[i];
+
+    var key = this.keyOption_ == goog.proto2.ObjectSerializer.KeyOption.NAME ?
+        field.getName() :
+        field.getTag();
+
+
+    if (message.has(field)) {
+      if (field.isRepeated()) {
+        var array = [];
+        objectValue[key] = array;
+
+        for (var j = 0; j < message.countOf(field); j++) {
+          array.push(this.getSerializedValue(field, message.get(field, j)));
+        }
+
+      } else {
+        objectValue[key] = this.getSerializedValue(field, message.get(field));
+      }
+    }
+  }
+
+  // Add the unknown fields, if any.
+  message.forEachUnknown(function(tag, value) { objectValue[tag] = value; });
+
+  return objectValue;
+};
+
+
+/** @override */
+goog.proto2.ObjectSerializer.prototype.getSerializedValue = function(
+    field, value) {
+
+  // Handle the case where a boolean should be serialized as 0/1.
+  // Some deserialization libraries, such as GWT, can use this notation.
+  if (this.serializeBooleanAsNumber_ &&
+      field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.BOOL &&
+      goog.isBoolean(value)) {
+    return value ? 1 : 0;
+  }
+
+  return goog.proto2.ObjectSerializer.base(
+      this, 'getSerializedValue', field, value);
+};
+
+
+/** @override */
+goog.proto2.ObjectSerializer.prototype.getDeserializedValue = function(
+    field, value) {
+
+  // Gracefully handle the case where a boolean is represented by 0/1.
+  // Some serialization libraries, such as GWT, can use this notation.
+  if (field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.BOOL &&
+      goog.isNumber(value)) {
+    return Boolean(value);
+  }
+
+  return goog.proto2.ObjectSerializer.base(
+      this, 'getDeserializedValue', field, value);
+};
+
+
+/**
+ * Deserializes a message from an object and places the
+ * data in the message.
+ *
+ * @param {goog.proto2.Message} message The message in which to
+ *     place the information.
+ * @param {*} data The data of the message.
+ * @override
+ */
+goog.proto2.ObjectSerializer.prototype.deserializeTo = function(message, data) {
+  var descriptor = message.getDescriptor();
+
+  for (var key in data) {
+    var field;
+    var value = data[key];
+
+    var isNumeric = goog.string.isNumeric(key);
+
+    if (isNumeric) {
+      field = descriptor.findFieldByTag(key);
+    } else {
+      // We must be in Key == NAME mode to lookup by name.
+      goog.asserts.assert(
+          this.keyOption_ == goog.proto2.ObjectSerializer.KeyOption.NAME,
+          'Key mode ' + this.keyOption_ + 'for key ' + key + ' is not ' +
+              goog.proto2.ObjectSerializer.KeyOption.NAME);
+
+      field = descriptor.findFieldByName(key);
+    }
+
+    if (field) {
+      if (field.isRepeated()) {
+        goog.asserts.assert(
+            goog.isArray(value),
+            'Value for repeated field ' + field + ' must be an array.');
+
+        for (var j = 0; j < value.length; j++) {
+          message.add(field, this.getDeserializedValue(field, value[j]));
+        }
+      } else {
+        goog.asserts.assert(
+            !goog.isArray(value),
+            'Value for non-repeated field ' + field + ' must not be an array.');
+        message.set(field, this.getDeserializedValue(field, value));
+      }
+    } else {
+      if (isNumeric) {
+        // We have an unknown field.
+        message.setUnknown(Number(key), value);
+      } else {
+        // Named fields must be present.
+        goog.asserts.fail('Failed to find field: ' + key);
+      }
+    }
+  }
+};
diff --git a/third_party/ink/closure/proto2/serializer.js b/third_party/ink/closure/proto2/serializer.js
new file mode 100644
index 0000000..eb976d74
--- /dev/null
+++ b/third_party/ink/closure/proto2/serializer.js
@@ -0,0 +1,198 @@
+// Copyright 2008 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 Base class for all Protocol Buffer 2 serializers.
+ * @author jschorr@google.com (Joseph Schorr)
+ */
+
+goog.provide('goog.proto2.Serializer');
+
+goog.require('goog.asserts');
+goog.require('goog.proto2.FieldDescriptor');
+goog.require('goog.proto2.Message');
+
+
+
+/**
+ * Abstract base class for PB2 serializers. A serializer is a class which
+ * implements the serialization and deserialization of a Protocol Buffer Message
+ * to/from a specific format.
+ *
+ * @constructor
+ */
+goog.proto2.Serializer = function() {};
+
+
+/**
+ * @define {boolean} Whether to decode and convert symbolic enum values to
+ * actual enum values or leave them as strings.
+ */
+goog.define('goog.proto2.Serializer.DECODE_SYMBOLIC_ENUMS', false);
+
+
+/**
+ * Serializes a message to the expected format.
+ *
+ * @param {goog.proto2.Message} message The message to be serialized.
+ *
+ * @return {*} The serialized form of the message.
+ */
+goog.proto2.Serializer.prototype.serialize = goog.abstractMethod;
+
+
+/**
+ * Returns the serialized form of the given value for the given field if the
+ * field is a Message or Group and returns the value unchanged otherwise, except
+ * for Infinity, -Infinity and NaN numerical values which are converted to
+ * string representation.
+ *
+ * @param {goog.proto2.FieldDescriptor} field The field from which this
+ *     value came.
+ *
+ * @param {*} value The value of the field.
+ *
+ * @return {*} The value.
+ * @protected
+ */
+goog.proto2.Serializer.prototype.getSerializedValue = function(field, value) {
+  if (field.isCompositeType()) {
+    return this.serialize(/** @type {goog.proto2.Message} */ (value));
+  } else if (goog.isNumber(value) && !isFinite(value)) {
+    return value.toString();
+  } else {
+    return value;
+  }
+};
+
+
+/**
+ * Deserializes a message from the expected format.
+ *
+ * @param {goog.proto2.Descriptor} descriptor The descriptor of the message
+ *     to be created.
+ * @param {*} data The data of the message.
+ *
+ * @return {!goog.proto2.Message} The message created.
+ */
+goog.proto2.Serializer.prototype.deserialize = function(descriptor, data) {
+  var message = descriptor.createMessageInstance();
+  this.deserializeTo(message, data);
+  goog.asserts.assert(message instanceof goog.proto2.Message);
+  return message;
+};
+
+
+/**
+ * Deserializes a message from the expected format and places the
+ * data in the message.
+ *
+ * @param {goog.proto2.Message} message The message in which to
+ *     place the information.
+ * @param {*} data The data of the message.
+ */
+goog.proto2.Serializer.prototype.deserializeTo = goog.abstractMethod;
+
+
+/**
+ * Returns the deserialized form of the given value for the given field if the
+ * field is a Message or Group and returns the value, converted or unchanged,
+ * for primitive field types otherwise.
+ *
+ * @param {goog.proto2.FieldDescriptor} field The field from which this
+ *     value came.
+ *
+ * @param {*} value The value of the field.
+ *
+ * @return {*} The value.
+ * @protected
+ */
+goog.proto2.Serializer.prototype.getDeserializedValue = function(field, value) {
+  // Composite types are deserialized recursively.
+  if (field.isCompositeType()) {
+    if (value instanceof goog.proto2.Message) {
+      return value;
+    }
+
+    return this.deserialize(field.getFieldMessageType(), value);
+  }
+
+  // Decode enum values.
+  if (field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.ENUM) {
+    // If it's a string, get enum value by name.
+    // NB: In order this feature to work, property renaming should be turned off
+    // for the respective enums.
+    if (goog.proto2.Serializer.DECODE_SYMBOLIC_ENUMS && goog.isString(value)) {
+      // enumType is a regular Javascript enum as defined in field's metadata.
+      var enumType = field.getNativeType();
+      if (enumType.hasOwnProperty(value)) {
+        return enumType[value];
+      }
+    }
+
+    // If it's a string containing a positive integer, this looks like a viable
+    // enum int value. Return as numeric.
+    if (goog.isString(value) &&
+        goog.proto2.Serializer.INTEGER_REGEX.test(value)) {
+      var numeric = Number(value);
+      if (numeric > 0) {
+        return numeric;
+      }
+    }
+
+    // Return unknown values as is for backward compatibility.
+    return value;
+  }
+
+  // Return the raw value if the field does not allow the JSON input to be
+  // converted.
+  if (!field.deserializationConversionPermitted()) {
+    return value;
+  }
+
+  // Convert to native type of field.  Return the converted value or fall
+  // through to return the raw value.  The JSON encoding of int64 value 123
+  // might be either the number 123 or the string "123".  The field native type
+  // could be either Number or String (depending on field options in the .proto
+  // file).  All four combinations should work correctly.
+  var nativeType = field.getNativeType();
+  if (nativeType === String) {
+    // JSON numbers can be converted to strings.
+    if (goog.isNumber(value)) {
+      return String(value);
+    }
+  } else if (nativeType === Number) {
+    // JSON strings are sometimes used for large integer numeric values, as well
+    // as Infinity, -Infinity and NaN.
+    if (goog.isString(value)) {
+      // Handle +/- Infinity and NaN values.
+      if (value === 'Infinity' || value === '-Infinity' || value === 'NaN') {
+        return Number(value);
+      }
+
+      // Validate the string.  If the string is not an integral number, we would
+      // rather have an assertion or error in the caller than a mysterious NaN
+      // value.
+      if (goog.proto2.Serializer.INTEGER_REGEX.test(value)) {
+        return Number(value);
+      }
+    }
+  }
+
+  return value;
+};
+
+
+/** @const {!RegExp} */
+goog.proto2.Serializer.INTEGER_REGEX = /^-?[0-9]+$/;
diff --git a/third_party/ink/closure/reflect/reflect.js b/third_party/ink/closure/reflect/reflect.js
new file mode 100644
index 0000000..b846fba6
--- /dev/null
+++ b/third_party/ink/closure/reflect/reflect.js
@@ -0,0 +1,139 @@
+// Copyright 2009 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 Useful compiler idioms.
+ *
+ * @author mgoodman@google.com (Mark Goodman)
+ * @author johnlenz@google.com (John Lenz)
+ */
+
+goog.provide('goog.reflect');
+
+
+/**
+ * Syntax for object literal casts.
+ * @see http://go/jscompiler-renaming
+ * @see https://goo.gl/CRs09P
+ *
+ * Use this if you have an object literal whose keys need to have the same names
+ * as the properties of some class even after they are renamed by the compiler.
+ *
+ * @param {!Function} type Type to cast to.
+ * @param {Object} object Object literal to cast.
+ * @return {Object} The object literal.
+ */
+goog.reflect.object = function(type, object) {
+  return object;
+};
+
+/**
+ * Syntax for renaming property strings.
+ * @see http://go/jscompiler-renaming
+ * @see https://goo.gl/CRs09P
+ *
+ * Use this if you have an need to access a property as a string, but want
+ * to also have the property renamed by the compiler. In contrast to
+ * goog.reflect.object, this method takes an instance of an object.
+ *
+ * Properties must be simple names (not qualified names).
+ *
+ * @param {string} prop Name of the property
+ * @param {!Object} object Instance of the object whose type will be used
+ *     for renaming
+ * @return {string} The renamed property.
+ */
+goog.reflect.objectProperty = function(prop, object) {
+  return prop;
+};
+
+/**
+ * To assert to the compiler that an operation is needed when it would
+ * otherwise be stripped. For example:
+ * <code>
+ *     // Force a layout
+ *     goog.reflect.sinkValue(dialog.offsetHeight);
+ * </code>
+ * @param {T} x
+ * @return {T}
+ * @template T
+ */
+goog.reflect.sinkValue = function(x) {
+  goog.reflect.sinkValue[' '](x);
+  return x;
+};
+
+
+/**
+ * The compiler should optimize this function away iff no one ever uses
+ * goog.reflect.sinkValue.
+ */
+goog.reflect.sinkValue[' '] = goog.nullFunction;
+
+
+/**
+ * Check if a property can be accessed without throwing an exception.
+ * @param {Object} obj The owner of the property.
+ * @param {string} prop The property name.
+ * @return {boolean} Whether the property is accessible. Will also return true
+ *     if obj is null.
+ */
+goog.reflect.canAccessProperty = function(obj, prop) {
+
+  try {
+    goog.reflect.sinkValue(obj[prop]);
+    return true;
+  } catch (e) {
+  }
+  return false;
+};
+
+
+/**
+ * Retrieves a value from a cache given a key. The compiler provides special
+ * consideration for this call such that it is generally considered side-effect
+ * free. However, if the {@code opt_keyFn} or {@code valueFn} have side-effects
+ * then the entire call is considered to have side-effects.
+ *
+ * Conventionally storing the value on the cache would be considered a
+ * side-effect and preclude unused calls from being pruned, ie. even if
+ * the value was never used, it would still always be stored in the cache.
+ *
+ * Providing a side-effect free {@code valueFn} and {@code opt_keyFn}
+ * allows unused calls to {@code goog.reflect.cache} to be pruned.
+ *
+ * @param {!Object<K, V>} cacheObj The object that contains the cached values.
+ * @param {?} key The key to lookup in the cache. If it is not string or number
+ *     then a {@code opt_keyFn} should be provided. The key is also used as the
+ *     parameter to the {@code valueFn}.
+ * @param {function(?):V} valueFn The value provider to use to calculate the
+ *     value to store in the cache. This function should be side-effect free
+ *     to take advantage of the optimization.
+ * @param {function(?):K=} opt_keyFn The key provider to determine the cache
+ *     map key. This should be used if the given key is not a string or number.
+ *     If not provided then the given key is used. This function should be
+ *     side-effect free to take advantage of the optimization.
+ * @return {V} The cached or calculated value.
+ * @template K
+ * @template V
+ */
+goog.reflect.cache = function(cacheObj, key, valueFn, opt_keyFn) {
+  var storedKey = opt_keyFn ? opt_keyFn(key) : key;
+
+  if (Object.prototype.hasOwnProperty.call(cacheObj, storedKey)) {
+    return cacheObj[storedKey];
+  }
+
+  return (cacheObj[storedKey] = valueFn(key));
+};
diff --git a/third_party/ink/closure/soy/data.js b/third_party/ink/closure/soy/data.js
new file mode 100644
index 0000000..24e3307
--- /dev/null
+++ b/third_party/ink/closure/soy/data.js
@@ -0,0 +1,525 @@
+// Copyright 2012 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 Soy data primitives.
+ *
+ * The goal is to encompass data types used by Soy, especially to mark content
+ * as known to be "safe".
+ *
+ * @author gboyer@google.com (Garrett Boyer)
+ */
+
+goog.provide('goog.soy.data.SanitizedContent');
+goog.provide('goog.soy.data.SanitizedContentKind');
+goog.provide('goog.soy.data.SanitizedCss');
+goog.provide('goog.soy.data.SanitizedHtml');
+goog.provide('goog.soy.data.SanitizedHtmlAttribute');
+goog.provide('goog.soy.data.SanitizedJs');
+goog.provide('goog.soy.data.SanitizedStyle');
+goog.provide('goog.soy.data.SanitizedTrustedResourceUri');
+goog.provide('goog.soy.data.SanitizedUri');
+goog.provide('goog.soy.data.UnsanitizedText');
+
+goog.require('goog.Uri');
+goog.require('goog.asserts');
+goog.require('goog.html.SafeHtml');
+goog.require('goog.html.SafeScript');
+goog.require('goog.html.SafeStyle');
+goog.require('goog.html.SafeStyleSheet');
+goog.require('goog.html.SafeUrl');
+goog.require('goog.html.TrustedResourceUrl');
+goog.require('goog.html.uncheckedconversions');
+goog.require('goog.i18n.bidi.Dir');
+goog.require('goog.string.Const');
+
+
+/**
+ * A type of textual content.
+ *
+ * This is an enum of type Object so that these values are unforgeable.
+ *
+ * @enum {!Object}
+ */
+goog.soy.data.SanitizedContentKind = {
+
+  /**
+   * A snippet of HTML that does not start or end inside a tag, comment, entity,
+   * or DOCTYPE; and that does not contain any executable code
+   * (JS, {@code <object>}s, etc.) from a different trust domain.
+   */
+  HTML: goog.DEBUG ? {sanitizedContentKindHtml: true} : {},
+
+  /**
+   * Executable Javascript code or expression, safe for insertion in a
+   * script-tag or event handler context, known to be free of any
+   * attacker-controlled scripts. This can either be side-effect-free
+   * Javascript (such as JSON) or Javascript that's entirely under Google's
+   * control.
+   */
+  JS: goog.DEBUG ? {sanitizedContentJsChars: true} : {},
+
+  /** A properly encoded portion of a URI. */
+  URI: goog.DEBUG ? {sanitizedContentUri: true} : {},
+
+  /** A resource URI not under attacker control. */
+  TRUSTED_RESOURCE_URI:
+      goog.DEBUG ? {sanitizedContentTrustedResourceUri: true} : {},
+
+  /**
+   * Repeated attribute names and values. For example,
+   * {@code dir="ltr" foo="bar" onclick="trustedFunction()" checked}.
+   */
+  ATTRIBUTES: goog.DEBUG ? {sanitizedContentHtmlAttribute: true} : {},
+
+  // TODO: Consider separating rules, declarations, and values into
+  // separate types, but for simplicity, we'll treat explicitly blessed
+  // SanitizedContent as allowed in all of these contexts.
+  /**
+   * A CSS3 declaration, property, value or group of semicolon separated
+   * declarations.
+   */
+  STYLE: goog.DEBUG ? {sanitizedContentStyle: true} : {},
+
+  /** A CSS3 style sheet (list of rules). */
+  CSS: goog.DEBUG ? {sanitizedContentCss: true} : {},
+
+  /**
+   * Unsanitized plain-text content.
+   *
+   * This is effectively the "null" entry of this enum, and is sometimes used
+   * to explicitly mark content that should never be used unescaped. Since any
+   * string is safe to use as text, being of ContentKind.TEXT makes no
+   * guarantees about its safety in any other context such as HTML.
+   */
+  TEXT: goog.DEBUG ? {sanitizedContentKindText: true} : {}
+};
+
+
+
+/**
+ * A string-like object that carries a content-type and a content direction.
+ *
+ * IMPORTANT! Do not create these directly, nor instantiate the subclasses.
+ * Instead, use a trusted, centrally reviewed library as endorsed by your team
+ * to generate these objects. Otherwise, you risk accidentally creating
+ * SanitizedContent that is attacker-controlled and gets evaluated unescaped in
+ * templates.
+ *
+ * @constructor
+ */
+goog.soy.data.SanitizedContent = function() {
+  throw new Error('Do not instantiate directly');
+};
+
+
+/**
+ * The context in which this content is safe from XSS attacks.
+ * @type {goog.soy.data.SanitizedContentKind}
+ */
+goog.soy.data.SanitizedContent.prototype.contentKind;
+
+
+/**
+ * The content's direction; null if unknown and thus to be estimated when
+ * necessary.
+ * @type {?goog.i18n.bidi.Dir}
+ */
+goog.soy.data.SanitizedContent.prototype.contentDir = null;
+
+
+/**
+ * The already-safe content.
+ * @protected {string}
+ */
+goog.soy.data.SanitizedContent.prototype.content;
+
+
+/**
+ * Gets the already-safe content.
+ * @return {string}
+ */
+goog.soy.data.SanitizedContent.prototype.getContent = function() {
+  return this.content;
+};
+
+
+/** @override */
+goog.soy.data.SanitizedContent.prototype.toString = function() {
+  return this.content;
+};
+
+
+/**
+ * Converts sanitized content of kind TEXT or HTML into SafeHtml. HTML content
+ * is converted without modification, while text content is HTML-escaped.
+ * @return {!goog.html.SafeHtml}
+ * @throws {Error} when the content kind is not TEXT or HTML.
+ */
+goog.soy.data.SanitizedContent.prototype.toSafeHtml = function() {
+  if (this.contentKind === goog.soy.data.SanitizedContentKind.TEXT) {
+    return goog.html.SafeHtml.htmlEscape(this.toString());
+  }
+  if (this.contentKind !== goog.soy.data.SanitizedContentKind.HTML) {
+    throw new Error('Sanitized content was not of kind TEXT or HTML.');
+  }
+  return goog.html.uncheckedconversions
+      .safeHtmlFromStringKnownToSatisfyTypeContract(
+          goog.string.Const.from(
+              'Soy SanitizedContent of kind HTML produces ' +
+              'SafeHtml-contract-compliant value.'),
+          this.toString(), this.contentDir);
+};
+
+
+/**
+ * Converts sanitized content of kind URI into SafeUrl without modification.
+ * @return {!goog.html.SafeUrl}
+ * @throws {Error} when the content kind is not URI.
+ */
+goog.soy.data.SanitizedContent.prototype.toSafeUrl = function() {
+  if (this.contentKind !== goog.soy.data.SanitizedContentKind.URI) {
+    throw new Error('Sanitized content was not of kind URI.');
+  }
+  return goog.html.uncheckedconversions
+      .safeUrlFromStringKnownToSatisfyTypeContract(
+          goog.string.Const.from(
+              'Soy SanitizedContent of kind URI produces ' +
+              'SafeHtml-contract-compliant value.'),
+          this.toString());
+};
+
+
+/**
+ * Unsanitized plain text string.
+ *
+ * While all strings are effectively safe to use as a plain text, there are no
+ * guarantees about safety in any other context such as HTML. This is
+ * sometimes used to mark that should never be used unescaped.
+ *
+ * @param {*} content Plain text with no guarantees.
+ * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if
+ *     unknown and thus to be estimated when necessary. Default: null.
+ * @extends {goog.soy.data.SanitizedContent}
+ * @constructor
+ */
+goog.soy.data.UnsanitizedText = function(content, opt_contentDir) {
+  // Not calling the superclass constructor which just throws an exception.
+
+  /** @override */
+  this.content = String(content);
+  this.contentDir = opt_contentDir != null ? opt_contentDir : null;
+};
+goog.inherits(goog.soy.data.UnsanitizedText, goog.soy.data.SanitizedContent);
+
+
+/** @override */
+goog.soy.data.UnsanitizedText.prototype.contentKind =
+    goog.soy.data.SanitizedContentKind.TEXT;
+
+
+
+/**
+ * Content of type {@link goog.soy.data.SanitizedContentKind.HTML}.
+ *
+ * The content is a string of HTML that can safely be embedded in a PCDATA
+ * context in your app.  If you would be surprised to find that an HTML
+ * sanitizer produced {@code s} (e.g.  it runs code or fetches bad URLs) and
+ * you wouldn't write a template that produces {@code s} on security or privacy
+ * grounds, then don't pass {@code s} here. The default content direction is
+ * unknown, i.e. to be estimated when necessary.
+ *
+ * @extends {goog.soy.data.SanitizedContent}
+ * @constructor
+ */
+goog.soy.data.SanitizedHtml = function() {
+  goog.soy.data.SanitizedHtml.base(this, 'constructor');
+};
+goog.inherits(goog.soy.data.SanitizedHtml, goog.soy.data.SanitizedContent);
+
+/** @override */
+goog.soy.data.SanitizedHtml.prototype.contentKind =
+    goog.soy.data.SanitizedContentKind.HTML;
+
+/**
+ * Checks if the value could be used as the Soy type {html}.
+ * @param {*} value
+ * @return {boolean}
+ */
+goog.soy.data.SanitizedHtml.isCompatibleWith = function(value) {
+  return goog.isString(value) ||
+      value instanceof goog.soy.data.SanitizedHtml ||
+      value instanceof goog.soy.data.UnsanitizedText ||
+      value instanceof goog.html.SafeHtml;
+};
+
+
+
+/**
+ * Content of type {@link goog.soy.data.SanitizedContentKind.JS}.
+ *
+ * The content is JavaScript source that when evaluated does not execute any
+ * attacker-controlled scripts. The content direction is LTR.
+ *
+ * @extends {goog.soy.data.SanitizedContent}
+ * @constructor
+ */
+goog.soy.data.SanitizedJs = function() {
+  goog.soy.data.SanitizedJs.base(this, 'constructor');
+};
+goog.inherits(goog.soy.data.SanitizedJs, goog.soy.data.SanitizedContent);
+
+/** @override */
+goog.soy.data.SanitizedJs.prototype.contentKind =
+    goog.soy.data.SanitizedContentKind.JS;
+
+/** @override */
+goog.soy.data.SanitizedJs.prototype.contentDir = goog.i18n.bidi.Dir.LTR;
+
+/**
+ * Checks if the value could be used as the Soy type {js}.
+ * @param {*} value
+ * @return {boolean}
+ */
+goog.soy.data.SanitizedJs.isCompatibleWith = function(value) {
+  return goog.isString(value) ||
+      value instanceof goog.soy.data.SanitizedJs ||
+      value instanceof goog.soy.data.UnsanitizedText ||
+      value instanceof goog.html.SafeScript;
+};
+
+
+
+/**
+ * Content of type {@link goog.soy.data.SanitizedContentKind.URI}.
+ *
+ * The content is a URI chunk that the caller knows is safe to emit in a
+ * template. The content direction is LTR.
+ *
+ * @extends {goog.soy.data.SanitizedContent}
+ * @constructor
+ */
+goog.soy.data.SanitizedUri = function() {
+  goog.soy.data.SanitizedUri.base(this, 'constructor');
+};
+goog.inherits(goog.soy.data.SanitizedUri, goog.soy.data.SanitizedContent);
+
+/** @override */
+goog.soy.data.SanitizedUri.prototype.contentKind =
+    goog.soy.data.SanitizedContentKind.URI;
+
+/** @override */
+goog.soy.data.SanitizedUri.prototype.contentDir = goog.i18n.bidi.Dir.LTR;
+
+/**
+ * Checks if the value could be used as the Soy type {uri}.
+ * @param {*} value
+ * @return {boolean}
+ */
+goog.soy.data.SanitizedUri.isCompatibleWith = function(value) {
+  return goog.isString(value) ||
+      value instanceof goog.soy.data.SanitizedUri ||
+      value instanceof goog.soy.data.UnsanitizedText ||
+      value instanceof goog.html.SafeUrl ||
+      value instanceof goog.html.TrustedResourceUrl ||
+      value instanceof goog.Uri;
+};
+
+
+
+/**
+ * Content of type
+ * {@link goog.soy.data.SanitizedContentKind.TRUSTED_RESOURCE_URI}.
+ *
+ * The content is a TrustedResourceUri chunk that is not under attacker control.
+ * The content direction is LTR.
+ *
+ * @extends {goog.soy.data.SanitizedContent}
+ * @constructor
+ */
+goog.soy.data.SanitizedTrustedResourceUri = function() {
+  goog.soy.data.SanitizedTrustedResourceUri.base(this, 'constructor');
+};
+goog.inherits(
+    goog.soy.data.SanitizedTrustedResourceUri, goog.soy.data.SanitizedContent);
+
+/** @override */
+goog.soy.data.SanitizedTrustedResourceUri.prototype.contentKind =
+    goog.soy.data.SanitizedContentKind.TRUSTED_RESOURCE_URI;
+
+/** @override */
+goog.soy.data.SanitizedTrustedResourceUri.prototype.contentDir =
+    goog.i18n.bidi.Dir.LTR;
+
+/**
+ * Converts sanitized content into TrustedResourceUrl without modification.
+ * @return {!goog.html.TrustedResourceUrl}
+ */
+goog.soy.data.SanitizedTrustedResourceUri.prototype.toTrustedResourceUrl =
+    function() {
+  return goog.html.uncheckedconversions
+      .trustedResourceUrlFromStringKnownToSatisfyTypeContract(
+          goog.string.Const.from(
+              'Soy SanitizedContent of kind TRUSTED_RESOURCE_URI produces ' +
+              'TrustedResourceUrl-contract-compliant value.'),
+          this.toString());
+};
+
+/**
+ * Checks if the value could be used as the Soy type {trusted_resource_uri}.
+ * @param {*} value
+ * @return {boolean}
+ */
+goog.soy.data.SanitizedTrustedResourceUri.isCompatibleWith = function(value) {
+  return goog.isString(value) ||
+      value instanceof goog.soy.data.SanitizedTrustedResourceUri ||
+      value instanceof goog.soy.data.UnsanitizedText ||
+      value instanceof goog.html.TrustedResourceUrl;
+};
+
+
+
+/**
+ * Content of type {@link goog.soy.data.SanitizedContentKind.ATTRIBUTES}.
+ *
+ * The content should be safely embeddable within an open tag, such as a
+ * key="value" pair. The content direction is LTR.
+ *
+ * @extends {goog.soy.data.SanitizedContent}
+ * @constructor
+ */
+goog.soy.data.SanitizedHtmlAttribute = function() {
+  goog.soy.data.SanitizedHtmlAttribute.base(this, 'constructor');
+};
+goog.inherits(
+    goog.soy.data.SanitizedHtmlAttribute, goog.soy.data.SanitizedContent);
+
+/** @override */
+goog.soy.data.SanitizedHtmlAttribute.prototype.contentKind =
+    goog.soy.data.SanitizedContentKind.ATTRIBUTES;
+
+/** @override */
+goog.soy.data.SanitizedHtmlAttribute.prototype.contentDir =
+    goog.i18n.bidi.Dir.LTR;
+
+/**
+ * Checks if the value could be used as the Soy type {attribute}.
+ * @param {*} value
+ * @return {boolean}
+ */
+goog.soy.data.SanitizedHtmlAttribute.isCompatibleWith = function(value) {
+  return goog.isString(value) ||
+      value instanceof goog.soy.data.SanitizedHtmlAttribute ||
+      value instanceof goog.soy.data.UnsanitizedText;
+};
+
+
+
+/**
+ * Content of type {@link goog.soy.data.SanitizedContentKind.STYLE}.
+ *
+ * The content is non-attacker-exploitable CSS, such as {@code color:#c3d9ff}.
+ * The content direction is LTR.
+ *
+ * @extends {goog.soy.data.SanitizedContent}
+ * @constructor
+ */
+goog.soy.data.SanitizedStyle = function() {
+  goog.soy.data.SanitizedStyle.base(this, 'constructor');
+};
+goog.inherits(goog.soy.data.SanitizedStyle, goog.soy.data.SanitizedContent);
+
+
+/** @override */
+goog.soy.data.SanitizedStyle.prototype.contentKind =
+    goog.soy.data.SanitizedContentKind.STYLE;
+
+
+/** @override */
+goog.soy.data.SanitizedStyle.prototype.contentDir = goog.i18n.bidi.Dir.LTR;
+
+
+/**
+ * Checks if the value could be used as the Soy type {css}.
+ * @param {*} value
+ * @return {boolean}
+ */
+goog.soy.data.SanitizedStyle.isCompatibleWith = function(value) {
+  return goog.isString(value) ||
+      value instanceof goog.soy.data.SanitizedStyle ||
+      value instanceof goog.soy.data.UnsanitizedText ||
+      value instanceof goog.html.SafeStyle;
+};
+
+
+
+/**
+ * Content of type {@link goog.soy.data.SanitizedContentKind.CSS}.
+ *
+ * The content is non-attacker-exploitable CSS, such as {@code @import url(x)}.
+ * The content direction is LTR.
+ *
+ * @extends {goog.soy.data.SanitizedContent}
+ * @constructor
+ */
+goog.soy.data.SanitizedCss = function() {
+  goog.soy.data.SanitizedCss.base(this, 'constructor');
+};
+goog.inherits(goog.soy.data.SanitizedCss, goog.soy.data.SanitizedContent);
+
+
+/** @override */
+goog.soy.data.SanitizedCss.prototype.contentKind =
+    goog.soy.data.SanitizedContentKind.CSS;
+
+
+/** @override */
+goog.soy.data.SanitizedCss.prototype.contentDir = goog.i18n.bidi.Dir.LTR;
+
+
+/**
+ * Checks if the value could be used as the Soy type {css}.
+ * @param {*} value
+ * @return {boolean}
+ */
+goog.soy.data.SanitizedCss.isCompatibleWith = function(value) {
+  return goog.isString(value) ||
+      value instanceof goog.soy.data.SanitizedCss ||
+      value instanceof goog.soy.data.UnsanitizedText ||
+      value instanceof goog.html.SafeStyle ||  // TODO(jakubvrana): Delete.
+      value instanceof goog.html.SafeStyleSheet;
+};
+
+
+/**
+ * Converts SanitizedCss into SafeStyleSheet.
+ * Note: SanitizedCss in Soy represents both SafeStyle and SafeStyleSheet in
+ * Closure. It's about to be split so that SanitizedCss represents only
+ * SafeStyleSheet.
+ * @return {!goog.html.SafeStyleSheet}
+ */
+goog.soy.data.SanitizedCss.prototype.toSafeStyleSheet = function() {
+  var value = this.toString();
+  // TODO(jakubvrana): Remove this check when there's a separate type for style
+  // declaration.
+  goog.asserts.assert(
+      /[@{]|^\s*$/.test(value),
+      'value doesn\'t look like style sheet: ' + value);
+  return goog.html.uncheckedconversions
+      .safeStyleSheetFromStringKnownToSatisfyTypeContract(
+          goog.string.Const.from(
+              'Soy SanitizedCss produces SafeStyleSheet-contract-compliant ' +
+              'value.'),
+          value);
+};
diff --git a/third_party/ink/closure/soy/soy.js b/third_party/ink/closure/soy/soy.js
new file mode 100644
index 0000000..e10dd5d
--- /dev/null
+++ b/third_party/ink/closure/soy/soy.js
@@ -0,0 +1,298 @@
+// 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 Provides utility methods to render soy template.
+ * @author kai@google.com (Kai Huang)
+ * @author ptucker@google.com (Philip Tucker)
+ * @author chrishenry@google.com (Chris Henry)
+ */
+
+goog.provide('goog.soy');
+
+goog.require('goog.asserts');
+goog.require('goog.dom');
+goog.require('goog.dom.NodeType');
+goog.require('goog.dom.TagName');
+goog.require('goog.html.legacyconversions');
+goog.require('goog.soy.data.SanitizedContent');
+goog.require('goog.soy.data.SanitizedContentKind');
+goog.require('goog.string');
+
+
+/**
+ * @define {boolean} Whether to require all Soy templates to be "strict html".
+ * Soy templates that use strict autoescaping forbid noAutoescape along with
+ * many dangerous directives, and return a runtime type SanitizedContent that
+ * marks them as safe.
+ *
+ * If this flag is enabled, Soy templates will fail to render if a template
+ * returns plain text -- indicating it is a non-strict template.
+ */
+goog.define('goog.soy.REQUIRE_STRICT_AUTOESCAPE', false);
+
+
+/**
+ * Type definition for strict Soy templates. Very useful when passing a template
+ * as an argument.
+ * @typedef {function(?, null=, ?Object<string, *>=):
+ *     !goog.soy.data.SanitizedContent}
+ */
+goog.soy.StrictTemplate;
+
+
+/**
+ * Type definition for strict Soy HTML templates. Very useful when passing
+ * a template as an argument.
+ * @typedef {function(?, null=, ?Object<string, *>=):
+ *     !goog.soy.data.SanitizedHtml}
+ */
+goog.soy.StrictHtmlTemplate;
+
+
+/**
+ * Sets the processed template as the innerHTML of an element. It is recommended
+ * to use this helper function instead of directly setting innerHTML in your
+ * hand-written code, so that it will be easier to audit the code for cross-site
+ * scripting vulnerabilities.
+ *
+ * @param {?Element} element The element whose content we are rendering into.
+ * @param {!goog.soy.data.SanitizedContent} templateResult The processed
+ *     template of kind HTML or TEXT (which will be escaped).
+ * @template ARG_TYPES
+ */
+goog.soy.renderHtml = function(element, templateResult) {
+  element.innerHTML = goog.soy.ensureTemplateOutputHtml_(templateResult);
+};
+
+
+/**
+ * Renders a Soy template and then set the output string as
+ * the innerHTML of an element. It is recommended to use this helper function
+ * instead of directly setting innerHTML in your hand-written code, so that it
+ * will be easier to audit the code for cross-site scripting vulnerabilities.
+ *
+ * @param {Element} element The element whose content we are rendering into.
+ * @param {?function(ARG_TYPES, Object<string, *>=):*|
+ *     ?function(ARG_TYPES, null=, Object<string, *>=):*} template
+ *     The Soy template defining the element's content.
+ * @param {ARG_TYPES=} opt_templateData The data for the template.
+ * @param {Object=} opt_injectedData The injected data for the template.
+ * @template ARG_TYPES
+ */
+goog.soy.renderElement = function(
+    element, template, opt_templateData, opt_injectedData) {
+  // Soy template parameter is only nullable for historical reasons.
+  goog.asserts.assert(template, 'Soy template may not be null.');
+  element.innerHTML = goog.soy.ensureTemplateOutputHtml_(
+      template(
+          opt_templateData || goog.soy.defaultTemplateData_, undefined,
+          opt_injectedData));
+};
+
+
+/**
+ * Renders a Soy template into a single node or a document
+ * fragment. If the rendered HTML string represents a single node, then that
+ * node is returned (note that this is *not* a fragment, despite them name of
+ * the method). Otherwise a document fragment is returned containing the
+ * rendered nodes.
+ *
+ * @param {?function(ARG_TYPES, Object<string, *>=):*|
+ *     ?function(ARG_TYPES, null=, Object<string, *>=):*} template
+ *     The Soy template defining the element's content.
+ * @param {ARG_TYPES=} opt_templateData The data for the template.
+ * @param {Object=} opt_injectedData The injected data for the template.
+ * @param {goog.dom.DomHelper=} opt_domHelper The DOM helper used to
+ *     create DOM nodes; defaults to {@code goog.dom.getDomHelper}.
+ * @return {!Node} The resulting node or document fragment.
+ * @template ARG_TYPES
+ */
+goog.soy.renderAsFragment = function(
+    template, opt_templateData, opt_injectedData, opt_domHelper) {
+  // Soy template parameter is only nullable for historical reasons.
+  goog.asserts.assert(template, 'Soy template may not be null.');
+  var dom = opt_domHelper || goog.dom.getDomHelper();
+  var output = template(
+      opt_templateData || goog.soy.defaultTemplateData_, undefined,
+      opt_injectedData);
+  var html = goog.soy.ensureTemplateOutputHtml_(output);
+  goog.soy.assertFirstTagValid_(html);
+  var safeHtml = output instanceof goog.soy.data.SanitizedContent ?
+      output.toSafeHtml() :
+      goog.html.legacyconversions.safeHtmlFromString(html);
+  return dom.safeHtmlToNode(safeHtml);
+};
+
+
+/**
+ * Renders a Soy template into a single node. If the rendered
+ * HTML string represents a single node, then that node is returned. Otherwise,
+ * a DIV element is returned containing the rendered nodes.
+ *
+ * @param {?function(ARG_TYPES, Object<string, *>=):*|
+ *     ?function(ARG_TYPES, null=, Object<string, *>=):*} template
+ *     The Soy template defining the element's content.
+ * @param {ARG_TYPES=} opt_templateData The data for the template.
+ * @param {Object=} opt_injectedData The injected data for the template.
+ * @param {goog.dom.DomHelper=} opt_domHelper The DOM helper used to
+ *     create DOM nodes; defaults to {@code goog.dom.getDomHelper}.
+ * @return {!Element} Rendered template contents, wrapped in a parent DIV
+ *     element if necessary.
+ * @template ARG_TYPES
+ */
+goog.soy.renderAsElement = function(
+    template, opt_templateData, opt_injectedData, opt_domHelper) {
+  // Soy template parameter is only nullable for historical reasons.
+  goog.asserts.assert(template, 'Soy template may not be null.');
+  return goog.soy.convertToElement_(
+      template(
+          opt_templateData || goog.soy.defaultTemplateData_, undefined,
+          opt_injectedData),
+      opt_domHelper);
+};
+
+
+/**
+ * Converts a processed Soy template into a single node. If the rendered
+ * HTML string represents a single node, then that node is returned. Otherwise,
+ * a DIV element is returned containing the rendered nodes.
+ *
+ * @param {!goog.soy.data.SanitizedContent} templateResult The processed
+ *     template of kind HTML or TEXT (which will be escaped).
+ * @param {?goog.dom.DomHelper=} opt_domHelper The DOM helper used to
+ *     create DOM nodes; defaults to {@code goog.dom.getDomHelper}.
+ * @return {!Element} Rendered template contents, wrapped in a parent DIV
+ *     element if necessary.
+ */
+goog.soy.convertToElement = function(templateResult, opt_domHelper) {
+  return goog.soy.convertToElement_(templateResult, opt_domHelper);
+};
+
+
+/**
+ * Non-strict version of {@code goog.soy.convertToElement}.
+ *
+ * @param {*} templateResult The processed template.
+ * @param {?goog.dom.DomHelper=} opt_domHelper The DOM helper used to
+ *     create DOM nodes; defaults to {@code goog.dom.getDomHelper}.
+ * @return {!Element} Rendered template contents, wrapped in a parent DIV
+ *     element if necessary.
+ * @private
+ */
+goog.soy.convertToElement_ = function(templateResult, opt_domHelper) {
+  var dom = opt_domHelper || goog.dom.getDomHelper();
+  var wrapper = dom.createElement(goog.dom.TagName.DIV);
+  var html = goog.soy.ensureTemplateOutputHtml_(templateResult);
+  goog.soy.assertFirstTagValid_(html);
+  wrapper.innerHTML = html;
+
+  // If the template renders as a single element, return it.
+  if (wrapper.childNodes.length == 1) {
+    var firstChild = wrapper.firstChild;
+    if (firstChild.nodeType == goog.dom.NodeType.ELEMENT) {
+      return /** @type {!Element} */ (firstChild);
+    }
+  }
+
+  // Otherwise, return the wrapper DIV.
+  return wrapper;
+};
+
+
+/**
+ * Ensures the result is "safe" to insert as HTML.
+ *
+ * Note if the template has non-strict autoescape, the guarantees here are very
+ * weak. It is recommended applications switch to requiring strict
+ * autoescaping over time by tweaking goog.soy.REQUIRE_STRICT_AUTOESCAPE.
+ *
+ * In the case the argument is a SanitizedContent object, it either must
+ * already be of kind HTML, or if it is kind="text", the output will be HTML
+ * escaped.
+ *
+ * @param {*} templateResult The template result.
+ * @return {string} The assumed-safe HTML output string.
+ * @private
+ */
+goog.soy.ensureTemplateOutputHtml_ = function(templateResult) {
+  // Allow strings as long as strict autoescaping is not mandated. Note we
+  // allow everything that isn't an object, because some non-escaping templates
+  // end up returning non-strings if their only print statement is a
+  // non-escaped argument, plus some unit tests spoof templates.
+  // TODO(gboyer): Track down and fix these cases.
+  if (!goog.soy.REQUIRE_STRICT_AUTOESCAPE && !goog.isObject(templateResult)) {
+    return String(templateResult);
+  }
+
+  // Allow SanitizedContent of kind HTML.
+  if (templateResult instanceof goog.soy.data.SanitizedContent) {
+    templateResult =
+        /** @type {!goog.soy.data.SanitizedContent} */ (templateResult);
+    var ContentKind = goog.soy.data.SanitizedContentKind;
+    if (templateResult.contentKind === ContentKind.HTML) {
+      return goog.asserts.assertString(templateResult.getContent());
+    }
+    if (templateResult.contentKind === ContentKind.TEXT) {
+      // Allow text to be rendered, as long as we escape it. Other content
+      // kinds will fail, since we don't know what to do with them.
+      // TODO(gboyer): Perhaps also include URI in this case.
+      return goog.string.htmlEscape(templateResult.getContent());
+    }
+  }
+
+  goog.asserts.fail(
+      'Soy template output is unsafe for use as HTML: ' + templateResult);
+
+  // In production, return a safe string, rather than failing hard.
+  return 'zSoyz';
+};
+
+
+/**
+ * Checks that the rendered HTML does not start with an invalid tag that would
+ * likely cause unexpected output from renderAsElement or renderAsFragment.
+ * See {@link http://www.w3.org/TR/html5/semantics.html#semantics} for reference
+ * as to which HTML elements can be parents of each other.
+ * @param {string} html The output of a template.
+ * @private
+ */
+goog.soy.assertFirstTagValid_ = function(html) {
+  if (goog.asserts.ENABLE_ASSERTS) {
+    var matches = html.match(goog.soy.INVALID_TAG_TO_RENDER_);
+    goog.asserts.assert(
+        !matches, 'This template starts with a %s, which ' +
+            'cannot be a child of a <div>, as required by soy internals. ' +
+            'Consider using goog.soy.renderElement instead.\nTemplate output: %s',
+        matches && matches[0], html);
+  }
+};
+
+
+/**
+ * A pattern to find templates that cannot be rendered by renderAsElement or
+ * renderAsFragment, as these elements cannot exist as the child of a <div>.
+ * @type {!RegExp}
+ * @private
+ */
+goog.soy.INVALID_TAG_TO_RENDER_ =
+    /^<(body|caption|col|colgroup|head|html|tr|td|th|tbody|thead|tfoot)>/i;
+
+
+/**
+ * Immutable object that is passed into templates that are rendered
+ * without any data.
+ * @private @const
+ */
+goog.soy.defaultTemplateData_ = {};
diff --git a/third_party/ink/closure/string/const.js b/third_party/ink/closure/string/const.js
new file mode 100644
index 0000000..30bfc4e
--- /dev/null
+++ b/third_party/ink/closure/string/const.js
@@ -0,0 +1,186 @@
+// Copyright 2013 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.
+
+goog.provide('goog.string.Const');
+
+goog.require('goog.asserts');
+goog.require('goog.string.TypedString');
+
+
+
+/**
+ * Wrapper for compile-time-constant strings.
+ *
+ * Const is a wrapper for strings that can only be created from program
+ * constants (i.e., string literals).  This property relies on a custom Closure
+ * compiler check that {@code goog.string.Const.from} is only invoked on
+ * compile-time-constant expressions.
+ *
+ * Const is useful in APIs whose correct and secure use requires that certain
+ * arguments are not attacker controlled: Compile-time constants are inherently
+ * under the control of the application and not under control of external
+ * attackers, and hence are safe to use in such contexts.
+ *
+ * Instances of this type must be created via its factory method
+ * {@code goog.string.Const.from} and not by invoking its constructor.  The
+ * constructor intentionally takes no parameters and the type is immutable;
+ * hence only a default instance corresponding to the empty string can be
+ * obtained via constructor invocation.
+ *
+ * @see goog.string.Const#from
+ * @constructor
+ * @final
+ * @struct
+ * @implements {goog.string.TypedString}
+ */
+goog.string.Const = function() {
+  /**
+   * The wrapped value of this Const object.  The field has a purposely ugly
+   * name to make (non-compiled) code that attempts to directly access this
+   * field stand out.
+   * @private {string}
+   */
+  this.stringConstValueWithSecurityContract__googStringSecurityPrivate_ = '';
+
+  /**
+   * A type marker used to implement additional run-time type checking.
+   * @see goog.string.Const#unwrap
+   * @const {!Object}
+   * @private
+   */
+  this.STRING_CONST_TYPE_MARKER__GOOG_STRING_SECURITY_PRIVATE_ =
+      goog.string.Const.TYPE_MARKER_;
+};
+
+
+/**
+ * @override
+ * @const
+ */
+goog.string.Const.prototype.implementsGoogStringTypedString = true;
+
+
+/**
+ * Returns this Const's value a string.
+ *
+ * IMPORTANT: In code where it is security-relevant that an object's type is
+ * indeed {@code goog.string.Const}, use {@code goog.string.Const.unwrap}
+ * instead of this method.
+ *
+ * @see goog.string.Const#unwrap
+ * @override
+ */
+goog.string.Const.prototype.getTypedStringValue = function() {
+  return this.stringConstValueWithSecurityContract__googStringSecurityPrivate_;
+};
+
+
+/**
+ * Returns a debug-string representation of this value.
+ *
+ * To obtain the actual string value wrapped inside an object of this type,
+ * use {@code goog.string.Const.unwrap}.
+ *
+ * @see goog.string.Const#unwrap
+ * @override
+ */
+goog.string.Const.prototype.toString = function() {
+  return 'Const{' +
+      this.stringConstValueWithSecurityContract__googStringSecurityPrivate_ +
+      '}';
+};
+
+
+/**
+ * Performs a runtime check that the provided object is indeed an instance
+ * of {@code goog.string.Const}, and returns its value.
+ * @param {!goog.string.Const} stringConst The object to extract from.
+ * @return {string} The Const object's contained string, unless the run-time
+ *     type check fails. In that case, {@code unwrap} returns an innocuous
+ *     string, or, if assertions are enabled, throws
+ *     {@code goog.asserts.AssertionError}.
+ */
+goog.string.Const.unwrap = function(stringConst) {
+  // Perform additional run-time type-checking to ensure that stringConst is
+  // indeed an instance of the expected type.  This provides some additional
+  // protection against security bugs due to application code that disables type
+  // checks.
+  if (stringConst instanceof goog.string.Const &&
+      stringConst.constructor === goog.string.Const &&
+      stringConst.STRING_CONST_TYPE_MARKER__GOOG_STRING_SECURITY_PRIVATE_ ===
+          goog.string.Const.TYPE_MARKER_) {
+    return stringConst
+        .stringConstValueWithSecurityContract__googStringSecurityPrivate_;
+  } else {
+    goog.asserts.fail(
+        'expected object of type Const, got \'' + stringConst + '\'');
+    return 'type_error:Const';
+  }
+};
+
+
+/**
+ * Creates a Const object from a compile-time constant string.
+ *
+ * It is illegal to invoke this function on an expression whose
+ * compile-time-contant value cannot be determined by the Closure compiler.
+ *
+ * Correct invocations include,
+ * <pre>
+ *   var s = goog.string.Const.from('hello');
+ *   var t = goog.string.Const.from('hello' + 'world');
+ * </pre>
+ *
+ * In contrast, the following are illegal:
+ * <pre>
+ *   var s = goog.string.Const.from(getHello());
+ *   var t = goog.string.Const.from('hello' + world);
+ * </pre>
+ *
+ * @param {string} s A constant string from which to create a Const.
+ * @return {!goog.string.Const} A Const object initialized to stringConst.
+ */
+goog.string.Const.from = function(s) {
+  return goog.string.Const.create__googStringSecurityPrivate_(s);
+};
+
+
+/**
+ * Type marker for the Const type, used to implement additional run-time
+ * type checking.
+ * @const {!Object}
+ * @private
+ */
+goog.string.Const.TYPE_MARKER_ = {};
+
+
+/**
+ * Utility method to create Const instances.
+ * @param {string} s The string to initialize the Const object with.
+ * @return {!goog.string.Const} The initialized Const object.
+ * @private
+ */
+goog.string.Const.create__googStringSecurityPrivate_ = function(s) {
+  var stringConst = new goog.string.Const();
+  stringConst.stringConstValueWithSecurityContract__googStringSecurityPrivate_ =
+      s;
+  return stringConst;
+};
+
+
+/**
+ * A Const instance wrapping the empty string.
+ * @const {!goog.string.Const}
+ */
+goog.string.Const.EMPTY = goog.string.Const.from('');
diff --git a/third_party/ink/closure/string/string.js b/third_party/ink/closure/string/string.js
new file mode 100644
index 0000000..8e8c1c4f
--- /dev/null
+++ b/third_party/ink/closure/string/string.js
@@ -0,0 +1,1642 @@
+// Copyright 2006 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 Utilities for string manipulation.
+ * @author pupius@google.com (Daniel Pupius)
+ * @author arv@google.com (Erik Arvidsson)
+ */
+
+
+/**
+ * Namespace for string utilities
+ */
+goog.provide('goog.string');
+goog.provide('goog.string.Unicode');
+
+
+/**
+ * @define {boolean} Enables HTML escaping of lowercase letter "e" which helps
+ * with detection of double-escaping as this letter is frequently used.
+ */
+goog.define('goog.string.DETECT_DOUBLE_ESCAPING', false);
+
+
+/**
+ * @define {boolean} Whether to force non-dom html unescaping.
+ */
+goog.define('goog.string.FORCE_NON_DOM_HTML_UNESCAPING', false);
+
+
+/**
+ * Common Unicode string characters.
+ * @enum {string}
+ */
+goog.string.Unicode = {
+  NBSP: '\xa0'
+};
+
+
+/**
+ * Fast prefix-checker.
+ * @param {string} str The string to check.
+ * @param {string} prefix A string to look for at the start of {@code str}.
+ * @return {boolean} True if {@code str} begins with {@code prefix}.
+ */
+goog.string.startsWith = function(str, prefix) {
+  return str.lastIndexOf(prefix, 0) == 0;
+};
+
+
+/**
+ * Fast suffix-checker.
+ * @param {string} str The string to check.
+ * @param {string} suffix A string to look for at the end of {@code str}.
+ * @return {boolean} True if {@code str} ends with {@code suffix}.
+ */
+goog.string.endsWith = function(str, suffix) {
+  var l = str.length - suffix.length;
+  return l >= 0 && str.indexOf(suffix, l) == l;
+};
+
+
+/**
+ * Case-insensitive prefix-checker.
+ * @param {string} str The string to check.
+ * @param {string} prefix  A string to look for at the end of {@code str}.
+ * @return {boolean} True if {@code str} begins with {@code prefix} (ignoring
+ *     case).
+ */
+goog.string.caseInsensitiveStartsWith = function(str, prefix) {
+  return goog.string.caseInsensitiveCompare(
+             prefix, str.substr(0, prefix.length)) == 0;
+};
+
+
+/**
+ * Case-insensitive suffix-checker.
+ * @param {string} str The string to check.
+ * @param {string} suffix A string to look for at the end of {@code str}.
+ * @return {boolean} True if {@code str} ends with {@code suffix} (ignoring
+ *     case).
+ */
+goog.string.caseInsensitiveEndsWith = function(str, suffix) {
+  return (
+      goog.string.caseInsensitiveCompare(
+          suffix, str.substr(str.length - suffix.length, suffix.length)) == 0);
+};
+
+
+/**
+ * Case-insensitive equality checker.
+ * @param {string} str1 First string to check.
+ * @param {string} str2 Second string to check.
+ * @return {boolean} True if {@code str1} and {@code str2} are the same string,
+ *     ignoring case.
+ */
+goog.string.caseInsensitiveEquals = function(str1, str2) {
+  return str1.toLowerCase() == str2.toLowerCase();
+};
+
+
+/**
+ * Does simple python-style string substitution.
+ * subs("foo%s hot%s", "bar", "dog") becomes "foobar hotdog".
+ * @param {string} str The string containing the pattern.
+ * @param {...*} var_args The items to substitute into the pattern.
+ * @return {string} A copy of {@code str} in which each occurrence of
+ *     {@code %s} has been replaced an argument from {@code var_args}.
+ */
+goog.string.subs = function(str, var_args) {
+  var splitParts = str.split('%s');
+  var returnString = '';
+
+  var subsArguments = Array.prototype.slice.call(arguments, 1);
+  while (subsArguments.length &&
+         // Replace up to the last split part. We are inserting in the
+         // positions between split parts.
+         splitParts.length > 1) {
+    returnString += splitParts.shift() + subsArguments.shift();
+  }
+
+  return returnString + splitParts.join('%s');  // Join unused '%s'
+};
+
+
+/**
+ * Converts multiple whitespace chars (spaces, non-breaking-spaces, new lines
+ * and tabs) to a single space, and strips leading and trailing whitespace.
+ * @param {string} str Input string.
+ * @return {string} A copy of {@code str} with collapsed whitespace.
+ */
+goog.string.collapseWhitespace = function(str) {
+  // Since IE doesn't include non-breaking-space (0xa0) in their \s character
+  // class (as required by section 7.2 of the ECMAScript spec), we explicitly
+  // include it in the regexp to enforce consistent cross-browser behavior.
+  return str.replace(/[\s\xa0]+/g, ' ').replace(/^\s+|\s+$/g, '');
+};
+
+
+/**
+ * Checks if a string is empty or contains only whitespaces.
+ * @param {string} str The string to check.
+ * @return {boolean} Whether {@code str} is empty or whitespace only.
+ */
+goog.string.isEmptyOrWhitespace = function(str) {
+  // testing length == 0 first is actually slower in all browsers (about the
+  // same in Opera).
+  // Since IE doesn't include non-breaking-space (0xa0) in their \s character
+  // class (as required by section 7.2 of the ECMAScript spec), we explicitly
+  // include it in the regexp to enforce consistent cross-browser behavior.
+  return /^[\s\xa0]*$/.test(str);
+};
+
+
+/**
+ * Checks if a string is empty.
+ * @param {string} str The string to check.
+ * @return {boolean} Whether {@code str} is empty.
+ */
+goog.string.isEmptyString = function(str) {
+  return str.length == 0;
+};
+
+
+/**
+ * Checks if a string is empty or contains only whitespaces.
+ *
+ * @param {string} str The string to check.
+ * @return {boolean} Whether {@code str} is empty or whitespace only.
+ * @deprecated Use goog.string.isEmptyOrWhitespace instead.
+ */
+goog.string.isEmpty = goog.string.isEmptyOrWhitespace;
+
+
+/**
+ * Checks if a string is null, undefined, empty or contains only whitespaces.
+ * @param {*} str The string to check.
+ * @return {boolean} Whether {@code str} is null, undefined, empty, or
+ *     whitespace only.
+ * @deprecated Use goog.string.isEmptyOrWhitespace(goog.string.makeSafe(str))
+ *     instead.
+ */
+goog.string.isEmptyOrWhitespaceSafe = function(str) {
+  return goog.string.isEmptyOrWhitespace(goog.string.makeSafe(str));
+};
+
+
+/**
+ * Checks if a string is null, undefined, empty or contains only whitespaces.
+ *
+ * @param {*} str The string to check.
+ * @return {boolean} Whether {@code str} is null, undefined, empty, or
+ *     whitespace only.
+ * @deprecated Use goog.string.isEmptyOrWhitespace instead.
+ */
+goog.string.isEmptySafe = goog.string.isEmptyOrWhitespaceSafe;
+
+
+/**
+ * Checks if a string is all breaking whitespace.
+ * @param {string} str The string to check.
+ * @return {boolean} Whether the string is all breaking whitespace.
+ */
+goog.string.isBreakingWhitespace = function(str) {
+  return !/[^\t\n\r ]/.test(str);
+};
+
+
+/**
+ * Checks if a string contains all letters.
+ * @param {string} str string to check.
+ * @return {boolean} True if {@code str} consists entirely of letters.
+ */
+goog.string.isAlpha = function(str) {
+  return !/[^a-zA-Z]/.test(str);
+};
+
+
+/**
+ * Checks if a string contains only numbers.
+ * @param {*} str string to check. If not a string, it will be
+ *     casted to one.
+ * @return {boolean} True if {@code str} is numeric.
+ */
+goog.string.isNumeric = function(str) {
+  return !/[^0-9]/.test(str);
+};
+
+
+/**
+ * Checks if a string contains only numbers or letters.
+ * @param {string} str string to check.
+ * @return {boolean} True if {@code str} is alphanumeric.
+ */
+goog.string.isAlphaNumeric = function(str) {
+  return !/[^a-zA-Z0-9]/.test(str);
+};
+
+
+/**
+ * Checks if a character is a space character.
+ * @param {string} ch Character to check.
+ * @return {boolean} True if {@code ch} is a space.
+ */
+goog.string.isSpace = function(ch) {
+  return ch == ' ';
+};
+
+
+/**
+ * Checks if a character is a valid unicode character.
+ * @param {string} ch Character to check.
+ * @return {boolean} True if {@code ch} is a valid unicode character.
+ */
+goog.string.isUnicodeChar = function(ch) {
+  return ch.length == 1 && ch >= ' ' && ch <= '~' ||
+      ch >= '\u0080' && ch <= '\uFFFD';
+};
+
+
+/**
+ * Takes a string and replaces newlines with a space. Multiple lines are
+ * replaced with a single space.
+ * @param {string} str The string from which to strip newlines.
+ * @return {string} A copy of {@code str} stripped of newlines.
+ */
+goog.string.stripNewlines = function(str) {
+  return str.replace(/(\r\n|\r|\n)+/g, ' ');
+};
+
+
+/**
+ * Replaces Windows and Mac new lines with unix style: \r or \r\n with \n.
+ * @param {string} str The string to in which to canonicalize newlines.
+ * @return {string} {@code str} A copy of {@code} with canonicalized newlines.
+ */
+goog.string.canonicalizeNewlines = function(str) {
+  return str.replace(/(\r\n|\r|\n)/g, '\n');
+};
+
+
+/**
+ * Normalizes whitespace in a string, replacing all whitespace chars with
+ * a space.
+ * @param {string} str The string in which to normalize whitespace.
+ * @return {string} A copy of {@code str} with all whitespace normalized.
+ */
+goog.string.normalizeWhitespace = function(str) {
+  return str.replace(/\xa0|\s/g, ' ');
+};
+
+
+/**
+ * Normalizes spaces in a string, replacing all consecutive spaces and tabs
+ * with a single space. Replaces non-breaking space with a space.
+ * @param {string} str The string in which to normalize spaces.
+ * @return {string} A copy of {@code str} with all consecutive spaces and tabs
+ *    replaced with a single space.
+ */
+goog.string.normalizeSpaces = function(str) {
+  return str.replace(/\xa0|[ \t]+/g, ' ');
+};
+
+
+/**
+ * Removes the breaking spaces from the left and right of the string and
+ * collapses the sequences of breaking spaces in the middle into single spaces.
+ * The original and the result strings render the same way in HTML.
+ * @param {string} str A string in which to collapse spaces.
+ * @return {string} Copy of the string with normalized breaking spaces.
+ */
+goog.string.collapseBreakingSpaces = function(str) {
+  return str.replace(/[\t\r\n ]+/g, ' ')
+      .replace(/^[\t\r\n ]+|[\t\r\n ]+$/g, '');
+};
+
+
+/**
+ * Trims white spaces to the left and right of a string.
+ * @param {string} str The string to trim.
+ * @return {string} A trimmed copy of {@code str}.
+ */
+goog.string.trim =
+    (goog.TRUSTED_SITE && String.prototype.trim) ? function(str) {
+      return str.trim();
+    } : function(str) {
+      // Since IE doesn't include non-breaking-space (0xa0) in their \s
+      // character class (as required by section 7.2 of the ECMAScript spec),
+      // we explicitly include it in the regexp to enforce consistent
+      // cross-browser behavior.
+      return str.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
+    };
+
+
+/**
+ * Trims whitespaces at the left end of a string.
+ * @param {string} str The string to left trim.
+ * @return {string} A trimmed copy of {@code str}.
+ */
+goog.string.trimLeft = function(str) {
+  // Since IE doesn't include non-breaking-space (0xa0) in their \s character
+  // class (as required by section 7.2 of the ECMAScript spec), we explicitly
+  // include it in the regexp to enforce consistent cross-browser behavior.
+  return str.replace(/^[\s\xa0]+/, '');
+};
+
+
+/**
+ * Trims whitespaces at the right end of a string.
+ * @param {string} str The string to right trim.
+ * @return {string} A trimmed copy of {@code str}.
+ */
+goog.string.trimRight = function(str) {
+  // Since IE doesn't include non-breaking-space (0xa0) in their \s character
+  // class (as required by section 7.2 of the ECMAScript spec), we explicitly
+  // include it in the regexp to enforce consistent cross-browser behavior.
+  return str.replace(/[\s\xa0]+$/, '');
+};
+
+
+/**
+ * A string comparator that ignores case.
+ * -1 = str1 less than str2
+ *  0 = str1 equals str2
+ *  1 = str1 greater than str2
+ *
+ * @param {string} str1 The string to compare.
+ * @param {string} str2 The string to compare {@code str1} to.
+ * @return {number} The comparator result, as described above.
+ */
+goog.string.caseInsensitiveCompare = function(str1, str2) {
+  var test1 = String(str1).toLowerCase();
+  var test2 = String(str2).toLowerCase();
+
+  if (test1 < test2) {
+    return -1;
+  } else if (test1 == test2) {
+    return 0;
+  } else {
+    return 1;
+  }
+};
+
+
+/**
+ * Compares two strings interpreting their numeric substrings as numbers.
+ *
+ * @param {string} str1 First string.
+ * @param {string} str2 Second string.
+ * @param {!RegExp} tokenizerRegExp Splits a string into substrings of
+ *     non-negative integers, non-numeric characters and optionally fractional
+ *     numbers starting with a decimal point.
+ * @return {number} Negative if str1 < str2, 0 is str1 == str2, positive if
+ *     str1 > str2.
+ * @private
+ */
+goog.string.numberAwareCompare_ = function(str1, str2, tokenizerRegExp) {
+  if (str1 == str2) {
+    return 0;
+  }
+  if (!str1) {
+    return -1;
+  }
+  if (!str2) {
+    return 1;
+  }
+
+  // Using match to split the entire string ahead of time turns out to be faster
+  // for most inputs than using RegExp.exec or iterating over each character.
+  var tokens1 = str1.toLowerCase().match(tokenizerRegExp);
+  var tokens2 = str2.toLowerCase().match(tokenizerRegExp);
+
+  var count = Math.min(tokens1.length, tokens2.length);
+
+  for (var i = 0; i < count; i++) {
+    var a = tokens1[i];
+    var b = tokens2[i];
+
+    // Compare pairs of tokens, returning if one token sorts before the other.
+    if (a != b) {
+      // Only if both tokens are integers is a special comparison required.
+      // Decimal numbers are sorted as strings (e.g., '.09' < '.1').
+      var num1 = parseInt(a, 10);
+      if (!isNaN(num1)) {
+        var num2 = parseInt(b, 10);
+        if (!isNaN(num2) && num1 - num2) {
+          return num1 - num2;
+        }
+      }
+      return a < b ? -1 : 1;
+    }
+  }
+
+  // If one string is a substring of the other, the shorter string sorts first.
+  if (tokens1.length != tokens2.length) {
+    return tokens1.length - tokens2.length;
+  }
+
+  // The two strings must be equivalent except for case (perfect equality is
+  // tested at the head of the function.) Revert to default ASCII string
+  // comparison to stabilize the sort.
+  return str1 < str2 ? -1 : 1;
+};
+
+
+/**
+ * String comparison function that handles non-negative integer numbers in a
+ * way humans might expect. Using this function, the string 'File 2.jpg' sorts
+ * before 'File 10.jpg', and 'Version 1.9' before 'Version 1.10'. The comparison
+ * is mostly case-insensitive, though strings that are identical except for case
+ * are sorted with the upper-case strings before lower-case.
+ *
+ * This comparison function is up to 50x slower than either the default or the
+ * case-insensitive compare. It should not be used in time-critical code, but
+ * should be fast enough to sort several hundred short strings (like filenames)
+ * with a reasonable delay.
+ *
+ * @param {string} str1 The string to compare in a numerically sensitive way.
+ * @param {string} str2 The string to compare {@code str1} to.
+ * @return {number} less than 0 if str1 < str2, 0 if str1 == str2, greater than
+ *     0 if str1 > str2.
+ */
+goog.string.intAwareCompare = function(str1, str2) {
+  return goog.string.numberAwareCompare_(str1, str2, /\d+|\D+/g);
+};
+
+
+/**
+ * String comparison function that handles non-negative integer and fractional
+ * numbers in a way humans might expect. Using this function, the string
+ * 'File 2.jpg' sorts before 'File 10.jpg', and '3.14' before '3.2'. Equivalent
+ * to {@link goog.string.intAwareCompare} apart from the way how it interprets
+ * dots.
+ *
+ * @param {string} str1 The string to compare in a numerically sensitive way.
+ * @param {string} str2 The string to compare {@code str1} to.
+ * @return {number} less than 0 if str1 < str2, 0 if str1 == str2, greater than
+ *     0 if str1 > str2.
+ */
+goog.string.floatAwareCompare = function(str1, str2) {
+  return goog.string.numberAwareCompare_(str1, str2, /\d+|\.\d+|\D+/g);
+};
+
+
+/**
+ * Alias for {@link goog.string.floatAwareCompare}.
+ *
+ * @param {string} str1
+ * @param {string} str2
+ * @return {number}
+ */
+goog.string.numerateCompare = goog.string.floatAwareCompare;
+
+
+/**
+ * URL-encodes a string
+ * @param {*} str The string to url-encode.
+ * @return {string} An encoded copy of {@code str} that is safe for urls.
+ *     Note that '#', ':', and other characters used to delimit portions
+ *     of URLs *will* be encoded.
+ */
+goog.string.urlEncode = function(str) {
+  return encodeURIComponent(String(str));
+};
+
+
+/**
+ * URL-decodes the string. We need to specially handle '+'s because
+ * the javascript library doesn't convert them to spaces.
+ * @param {string} str The string to url decode.
+ * @return {string} The decoded {@code str}.
+ */
+goog.string.urlDecode = function(str) {
+  return decodeURIComponent(str.replace(/\+/g, ' '));
+};
+
+
+/**
+ * Converts \n to <br>s or <br />s.
+ * @param {string} str The string in which to convert newlines.
+ * @param {boolean=} opt_xml Whether to use XML compatible tags.
+ * @return {string} A copy of {@code str} with converted newlines.
+ */
+goog.string.newLineToBr = function(str, opt_xml) {
+  return str.replace(/(\r\n|\r|\n)/g, opt_xml ? '<br />' : '<br>');
+};
+
+
+/**
+ * Escapes double quote '"' and single quote '\'' characters in addition to
+ * '&', '<', and '>' so that a string can be included in an HTML tag attribute
+ * value within double or single quotes.
+ *
+ * It should be noted that > doesn't need to be escaped for the HTML or XML to
+ * be valid, but it has been decided to escape it for consistency with other
+ * implementations.
+ *
+ * With goog.string.DETECT_DOUBLE_ESCAPING, this function escapes also the
+ * lowercase letter "e".
+ *
+ * NOTE(pupius):
+ * HtmlEscape is often called during the generation of large blocks of HTML.
+ * Using statics for the regular expressions and strings is an optimization
+ * that can more than half the amount of time IE spends in this function for
+ * large apps, since strings and regexes both contribute to GC allocations.
+ *
+ * Testing for the presence of a character before escaping increases the number
+ * of function calls, but actually provides a speed increase for the average
+ * case -- since the average case often doesn't require the escaping of all 4
+ * characters and indexOf() is much cheaper than replace().
+ * The worst case does suffer slightly from the additional calls, therefore the
+ * opt_isLikelyToContainHtmlChars option has been included for situations
+ * where all 4 HTML entities are very likely to be present and need escaping.
+ *
+ * Some benchmarks (times tended to fluctuate +-0.05ms):
+ *                                     FireFox                     IE6
+ * (no chars / average (mix of cases) / all 4 chars)
+ * no checks                     0.13 / 0.22 / 0.22         0.23 / 0.53 / 0.80
+ * indexOf                       0.08 / 0.17 / 0.26         0.22 / 0.54 / 0.84
+ * indexOf + re test             0.07 / 0.17 / 0.28         0.19 / 0.50 / 0.85
+ *
+ * An additional advantage of checking if replace actually needs to be called
+ * is a reduction in the number of object allocations, so as the size of the
+ * application grows the difference between the various methods would increase.
+ *
+ * @param {string} str string to be escaped.
+ * @param {boolean=} opt_isLikelyToContainHtmlChars Don't perform a check to see
+ *     if the character needs replacing - use this option if you expect each of
+ *     the characters to appear often. Leave false if you expect few html
+ *     characters to occur in your strings, such as if you are escaping HTML.
+ * @return {string} An escaped copy of {@code str}.
+ */
+goog.string.htmlEscape = function(str, opt_isLikelyToContainHtmlChars) {
+
+  if (opt_isLikelyToContainHtmlChars) {
+    str = str.replace(goog.string.AMP_RE_, '&amp;')
+              .replace(goog.string.LT_RE_, '&lt;')
+              .replace(goog.string.GT_RE_, '&gt;')
+              .replace(goog.string.QUOT_RE_, '&quot;')
+              .replace(goog.string.SINGLE_QUOTE_RE_, '&#39;')
+              .replace(goog.string.NULL_RE_, '&#0;');
+    if (goog.string.DETECT_DOUBLE_ESCAPING) {
+      str = str.replace(goog.string.E_RE_, '&#101;');
+    }
+    return str;
+
+  } else {
+    // quick test helps in the case when there are no chars to replace, in
+    // worst case this makes barely a difference to the time taken
+    if (!goog.string.ALL_RE_.test(str)) return str;
+
+    // str.indexOf is faster than regex.test in this case
+    if (str.indexOf('&') != -1) {
+      str = str.replace(goog.string.AMP_RE_, '&amp;');
+    }
+    if (str.indexOf('<') != -1) {
+      str = str.replace(goog.string.LT_RE_, '&lt;');
+    }
+    if (str.indexOf('>') != -1) {
+      str = str.replace(goog.string.GT_RE_, '&gt;');
+    }
+    if (str.indexOf('"') != -1) {
+      str = str.replace(goog.string.QUOT_RE_, '&quot;');
+    }
+    if (str.indexOf('\'') != -1) {
+      str = str.replace(goog.string.SINGLE_QUOTE_RE_, '&#39;');
+    }
+    if (str.indexOf('\x00') != -1) {
+      str = str.replace(goog.string.NULL_RE_, '&#0;');
+    }
+    if (goog.string.DETECT_DOUBLE_ESCAPING && str.indexOf('e') != -1) {
+      str = str.replace(goog.string.E_RE_, '&#101;');
+    }
+    return str;
+  }
+};
+
+
+/**
+ * Regular expression that matches an ampersand, for use in escaping.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.AMP_RE_ = /&/g;
+
+
+/**
+ * Regular expression that matches a less than sign, for use in escaping.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.LT_RE_ = /</g;
+
+
+/**
+ * Regular expression that matches a greater than sign, for use in escaping.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.GT_RE_ = />/g;
+
+
+/**
+ * Regular expression that matches a double quote, for use in escaping.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.QUOT_RE_ = /"/g;
+
+
+/**
+ * Regular expression that matches a single quote, for use in escaping.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.SINGLE_QUOTE_RE_ = /'/g;
+
+
+/**
+ * Regular expression that matches null character, for use in escaping.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.NULL_RE_ = /\x00/g;
+
+
+/**
+ * Regular expression that matches a lowercase letter "e", for use in escaping.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.E_RE_ = /e/g;
+
+
+/**
+ * Regular expression that matches any character that needs to be escaped.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.ALL_RE_ =
+    (goog.string.DETECT_DOUBLE_ESCAPING ? /[\x00&<>"'e]/ : /[\x00&<>"']/);
+
+
+/**
+ * Unescapes an HTML string.
+ *
+ * @param {string} str The string to unescape.
+ * @return {string} An unescaped copy of {@code str}.
+ */
+goog.string.unescapeEntities = function(str) {
+  if (goog.string.contains(str, '&')) {
+    // We are careful not to use a DOM if we do not have one or we explicitly
+    // requested non-DOM html unescaping.
+    if (!goog.string.FORCE_NON_DOM_HTML_UNESCAPING &&
+        'document' in goog.global) {
+      return goog.string.unescapeEntitiesUsingDom_(str);
+    } else {
+      // Fall back on pure XML entities
+      return goog.string.unescapePureXmlEntities_(str);
+    }
+  }
+  return str;
+};
+
+
+/**
+ * Unescapes a HTML string using the provided document.
+ *
+ * @param {string} str The string to unescape.
+ * @param {!Document} document A document to use in escaping the string.
+ * @return {string} An unescaped copy of {@code str}.
+ */
+goog.string.unescapeEntitiesWithDocument = function(str, document) {
+  if (goog.string.contains(str, '&')) {
+    return goog.string.unescapeEntitiesUsingDom_(str, document);
+  }
+  return str;
+};
+
+
+/**
+ * Unescapes an HTML string using a DOM to resolve non-XML, non-numeric
+ * entities. This function is XSS-safe and whitespace-preserving.
+ * @private
+ * @param {string} str The string to unescape.
+ * @param {Document=} opt_document An optional document to use for creating
+ *     elements. If this is not specified then the default window.document
+ *     will be used.
+ * @return {string} The unescaped {@code str} string.
+ */
+goog.string.unescapeEntitiesUsingDom_ = function(str, opt_document) {
+  /** @type {!Object<string, string>} */
+  var seen = {'&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"'};
+  var div;
+  if (opt_document) {
+    div = opt_document.createElement('div');
+  } else {
+    div = goog.global.document.createElement('div');
+  }
+  // Match as many valid entity characters as possible. If the actual entity
+  // happens to be shorter, it will still work as innerHTML will return the
+  // trailing characters unchanged. Since the entity characters do not include
+  // open angle bracket, there is no chance of XSS from the innerHTML use.
+  // Since no whitespace is passed to innerHTML, whitespace is preserved.
+  return str.replace(goog.string.HTML_ENTITY_PATTERN_, function(s, entity) {
+    // Check for cached entity.
+    var value = seen[s];
+    if (value) {
+      return value;
+    }
+    // Check for numeric entity.
+    if (entity.charAt(0) == '#') {
+      // Prefix with 0 so that hex entities (e.g. &#x10) parse as hex numbers.
+      var n = Number('0' + entity.substr(1));
+      if (!isNaN(n)) {
+        value = String.fromCharCode(n);
+      }
+    }
+    // Fall back to innerHTML otherwise.
+    if (!value) {
+      // Append a non-entity character to avoid a bug in Webkit that parses
+      // an invalid entity at the end of innerHTML text as the empty string.
+      div.innerHTML = s + ' ';
+      // Then remove the trailing character from the result.
+      value = div.firstChild.nodeValue.slice(0, -1);
+    }
+    // Cache and return.
+    return seen[s] = value;
+  });
+};
+
+
+/**
+ * Unescapes XML entities.
+ * @private
+ * @param {string} str The string to unescape.
+ * @return {string} An unescaped copy of {@code str}.
+ */
+goog.string.unescapePureXmlEntities_ = function(str) {
+  return str.replace(/&([^;]+);/g, function(s, entity) {
+    switch (entity) {
+      case 'amp':
+        return '&';
+      case 'lt':
+        return '<';
+      case 'gt':
+        return '>';
+      case 'quot':
+        return '"';
+      default:
+        if (entity.charAt(0) == '#') {
+          // Prefix with 0 so that hex entities (e.g. &#x10) parse as hex.
+          var n = Number('0' + entity.substr(1));
+          if (!isNaN(n)) {
+            return String.fromCharCode(n);
+          }
+        }
+        // For invalid entities we just return the entity
+        return s;
+    }
+  });
+};
+
+
+/**
+ * Regular expression that matches an HTML entity.
+ * See also HTML5: Tokenization / Tokenizing character references.
+ * @private
+ * @type {!RegExp}
+ */
+goog.string.HTML_ENTITY_PATTERN_ = /&([^;\s<&]+);?/g;
+
+
+/**
+ * Do escaping of whitespace to preserve spatial formatting. We use character
+ * entity #160 to make it safer for xml.
+ * @param {string} str The string in which to escape whitespace.
+ * @param {boolean=} opt_xml Whether to use XML compatible tags.
+ * @return {string} An escaped copy of {@code str}.
+ */
+goog.string.whitespaceEscape = function(str, opt_xml) {
+  // This doesn't use goog.string.preserveSpaces for backwards compatibility.
+  return goog.string.newLineToBr(str.replace(/  /g, ' &#160;'), opt_xml);
+};
+
+
+/**
+ * Preserve spaces that would be otherwise collapsed in HTML by replacing them
+ * with non-breaking space Unicode characters.
+ * @param {string} str The string in which to preserve whitespace.
+ * @return {string} A copy of {@code str} with preserved whitespace.
+ */
+goog.string.preserveSpaces = function(str) {
+  return str.replace(/(^|[\n ]) /g, '$1' + goog.string.Unicode.NBSP);
+};
+
+
+/**
+ * Strip quote characters around a string.  The second argument is a string of
+ * characters to treat as quotes.  This can be a single character or a string of
+ * multiple character and in that case each of those are treated as possible
+ * quote characters. For example:
+ *
+ * <pre>
+ * goog.string.stripQuotes('"abc"', '"`') --> 'abc'
+ * goog.string.stripQuotes('`abc`', '"`') --> 'abc'
+ * </pre>
+ *
+ * @param {string} str The string to strip.
+ * @param {string} quoteChars The quote characters to strip.
+ * @return {string} A copy of {@code str} without the quotes.
+ */
+goog.string.stripQuotes = function(str, quoteChars) {
+  var length = quoteChars.length;
+  for (var i = 0; i < length; i++) {
+    var quoteChar = length == 1 ? quoteChars : quoteChars.charAt(i);
+    if (str.charAt(0) == quoteChar && str.charAt(str.length - 1) == quoteChar) {
+      return str.substring(1, str.length - 1);
+    }
+  }
+  return str;
+};
+
+
+/**
+ * Truncates a string to a certain length and adds '...' if necessary.  The
+ * length also accounts for the ellipsis, so a maximum length of 10 and a string
+ * 'Hello World!' produces 'Hello W...'.
+ * @param {string} str The string to truncate.
+ * @param {number} chars Max number of characters.
+ * @param {boolean=} opt_protectEscapedCharacters Whether to protect escaped
+ *     characters from being cut off in the middle.
+ * @return {string} The truncated {@code str} string.
+ */
+goog.string.truncate = function(str, chars, opt_protectEscapedCharacters) {
+  if (opt_protectEscapedCharacters) {
+    str = goog.string.unescapeEntities(str);
+  }
+
+  if (str.length > chars) {
+    str = str.substring(0, chars - 3) + '...';
+  }
+
+  if (opt_protectEscapedCharacters) {
+    str = goog.string.htmlEscape(str);
+  }
+
+  return str;
+};
+
+
+/**
+ * Truncate a string in the middle, adding "..." if necessary,
+ * and favoring the beginning of the string.
+ * @param {string} str The string to truncate the middle of.
+ * @param {number} chars Max number of characters.
+ * @param {boolean=} opt_protectEscapedCharacters Whether to protect escaped
+ *     characters from being cutoff in the middle.
+ * @param {number=} opt_trailingChars Optional number of trailing characters to
+ *     leave at the end of the string, instead of truncating as close to the
+ *     middle as possible.
+ * @return {string} A truncated copy of {@code str}.
+ */
+goog.string.truncateMiddle = function(
+    str, chars, opt_protectEscapedCharacters, opt_trailingChars) {
+  if (opt_protectEscapedCharacters) {
+    str = goog.string.unescapeEntities(str);
+  }
+
+  if (opt_trailingChars && str.length > chars) {
+    if (opt_trailingChars > chars) {
+      opt_trailingChars = chars;
+    }
+    var endPoint = str.length - opt_trailingChars;
+    var startPoint = chars - opt_trailingChars;
+    str = str.substring(0, startPoint) + '...' + str.substring(endPoint);
+  } else if (str.length > chars) {
+    // Favor the beginning of the string:
+    var half = Math.floor(chars / 2);
+    var endPos = str.length - half;
+    half += chars % 2;
+    str = str.substring(0, half) + '...' + str.substring(endPos);
+  }
+
+  if (opt_protectEscapedCharacters) {
+    str = goog.string.htmlEscape(str);
+  }
+
+  return str;
+};
+
+
+/**
+ * Special chars that need to be escaped for goog.string.quote.
+ * @private {!Object<string, string>}
+ */
+goog.string.specialEscapeChars_ = {
+  '\0': '\\0',
+  '\b': '\\b',
+  '\f': '\\f',
+  '\n': '\\n',
+  '\r': '\\r',
+  '\t': '\\t',
+  '\x0B': '\\x0B',  // '\v' is not supported in JScript
+  '"': '\\"',
+  '\\': '\\\\',
+  // To support the use case of embedding quoted strings inside of script
+  // tags, we have to make sure HTML comments and opening/closing script tags do
+  // not appear in the resulting string. The specific strings that must be
+  // escaped are documented at:
+  // http://www.w3.org/TR/html51/semantics.html#restrictions-for-contents-of-script-elements
+  '<': '\x3c'
+};
+
+
+/**
+ * Character mappings used internally for goog.string.escapeChar.
+ * @private {!Object<string, string>}
+ */
+goog.string.jsEscapeCache_ = {
+  '\'': '\\\''
+};
+
+
+/**
+ * Encloses a string in double quotes and escapes characters so that the
+ * string is a valid JS string. The resulting string is safe to embed in
+ * `<script>` tags as "<" is escaped.
+ * @param {string} s The string to quote.
+ * @return {string} A copy of {@code s} surrounded by double quotes.
+ */
+goog.string.quote = function(s) {
+  s = String(s);
+  var sb = ['"'];
+  for (var i = 0; i < s.length; i++) {
+    var ch = s.charAt(i);
+    var cc = ch.charCodeAt(0);
+    sb[i + 1] = goog.string.specialEscapeChars_[ch] ||
+        ((cc > 31 && cc < 127) ? ch : goog.string.escapeChar(ch));
+  }
+  sb.push('"');
+  return sb.join('');
+};
+
+
+/**
+ * Takes a string and returns the escaped string for that input string.
+ * @param {string} str The string to escape.
+ * @return {string} An escaped string representing {@code str}.
+ */
+goog.string.escapeString = function(str) {
+  var sb = [];
+  for (var i = 0; i < str.length; i++) {
+    sb[i] = goog.string.escapeChar(str.charAt(i));
+  }
+  return sb.join('');
+};
+
+
+/**
+ * Takes a character and returns the escaped string for that character. For
+ * example escapeChar(String.fromCharCode(15)) -> "\\x0E".
+ * @param {string} c The character to escape.
+ * @return {string} An escaped string representing {@code c}.
+ */
+goog.string.escapeChar = function(c) {
+  if (c in goog.string.jsEscapeCache_) {
+    return goog.string.jsEscapeCache_[c];
+  }
+
+  if (c in goog.string.specialEscapeChars_) {
+    return goog.string.jsEscapeCache_[c] = goog.string.specialEscapeChars_[c];
+  }
+
+  var rv = c;
+  var cc = c.charCodeAt(0);
+  if (cc > 31 && cc < 127) {
+    rv = c;
+  } else {
+    // tab is 9 but handled above
+    if (cc < 256) {
+      rv = '\\x';
+      if (cc < 16 || cc > 256) {
+        rv += '0';
+      }
+    } else {
+      rv = '\\u';
+      if (cc < 4096) {  // \u1000
+        rv += '0';
+      }
+    }
+    rv += cc.toString(16).toUpperCase();
+  }
+
+  return goog.string.jsEscapeCache_[c] = rv;
+};
+
+
+/**
+ * Determines whether a string contains a substring.
+ * @param {string} str The string to search.
+ * @param {string} subString The substring to search for.
+ * @return {boolean} Whether {@code str} contains {@code subString}.
+ */
+goog.string.contains = function(str, subString) {
+  return str.indexOf(subString) != -1;
+};
+
+
+/**
+ * Determines whether a string contains a substring, ignoring case.
+ * @param {string} str The string to search.
+ * @param {string} subString The substring to search for.
+ * @return {boolean} Whether {@code str} contains {@code subString}.
+ */
+goog.string.caseInsensitiveContains = function(str, subString) {
+  return goog.string.contains(str.toLowerCase(), subString.toLowerCase());
+};
+
+
+/**
+ * Returns the non-overlapping occurrences of ss in s.
+ * If either s or ss evalutes to false, then returns zero.
+ * @param {string} s The string to look in.
+ * @param {string} ss The string to look for.
+ * @return {number} Number of occurrences of ss in s.
+ */
+goog.string.countOf = function(s, ss) {
+  return s && ss ? s.split(ss).length - 1 : 0;
+};
+
+
+/**
+ * Removes a substring of a specified length at a specific
+ * index in a string.
+ * @param {string} s The base string from which to remove.
+ * @param {number} index The index at which to remove the substring.
+ * @param {number} stringLength The length of the substring to remove.
+ * @return {string} A copy of {@code s} with the substring removed or the full
+ *     string if nothing is removed or the input is invalid.
+ */
+goog.string.removeAt = function(s, index, stringLength) {
+  var resultStr = s;
+  // If the index is greater or equal to 0 then remove substring
+  if (index >= 0 && index < s.length && stringLength > 0) {
+    resultStr = s.substr(0, index) +
+        s.substr(index + stringLength, s.length - index - stringLength);
+  }
+  return resultStr;
+};
+
+
+/**
+ * Removes the first occurrence of a substring from a string.
+ * @param {string} str The base string from which to remove.
+ * @param {string} substr The string to remove.
+ * @return {string} A copy of {@code str} with {@code substr} removed or the
+ *     full string if nothing is removed.
+ */
+goog.string.remove = function(str, substr) {
+  return str.replace(substr, '');
+};
+
+
+/**
+ *  Removes all occurrences of a substring from a string.
+ *  @param {string} s The base string from which to remove.
+ *  @param {string} ss The string to remove.
+ *  @return {string} A copy of {@code s} with {@code ss} removed or the full
+ *      string if nothing is removed.
+ */
+goog.string.removeAll = function(s, ss) {
+  var re = new RegExp(goog.string.regExpEscape(ss), 'g');
+  return s.replace(re, '');
+};
+
+
+/**
+ *  Replaces all occurrences of a substring of a string with a new substring.
+ *  @param {string} s The base string from which to remove.
+ *  @param {string} ss The string to replace.
+ *  @param {string} replacement The replacement string.
+ *  @return {string} A copy of {@code s} with {@code ss} replaced by
+ *      {@code replacement} or the original string if nothing is replaced.
+ */
+goog.string.replaceAll = function(s, ss, replacement) {
+  var re = new RegExp(goog.string.regExpEscape(ss), 'g');
+  return s.replace(re, replacement.replace(/\$/g, '$$$$'));
+};
+
+
+/**
+ * Escapes characters in the string that are not safe to use in a RegExp.
+ * @param {*} s The string to escape. If not a string, it will be casted
+ *     to one.
+ * @return {string} A RegExp safe, escaped copy of {@code s}.
+ */
+goog.string.regExpEscape = function(s) {
+  return String(s)
+      .replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1')
+      .replace(/\x08/g, '\\x08');
+};
+
+
+/**
+ * Repeats a string n times.
+ * @param {string} string The string to repeat.
+ * @param {number} length The number of times to repeat.
+ * @return {string} A string containing {@code length} repetitions of
+ *     {@code string}.
+ */
+goog.string.repeat = (String.prototype.repeat) ? function(string, length) {
+  // The native method is over 100 times faster than the alternative.
+  return string.repeat(length);
+} : function(string, length) {
+  return new Array(length + 1).join(string);
+};
+
+
+/**
+ * Pads number to given length and optionally rounds it to a given precision.
+ * For example:
+ * <pre>padNumber(1.25, 2, 3) -> '01.250'
+ * padNumber(1.25, 2) -> '01.25'
+ * padNumber(1.25, 2, 1) -> '01.3'
+ * padNumber(1.25, 0) -> '1.25'</pre>
+ *
+ * @param {number} num The number to pad.
+ * @param {number} length The desired length.
+ * @param {number=} opt_precision The desired precision.
+ * @return {string} {@code num} as a string with the given options.
+ */
+goog.string.padNumber = function(num, length, opt_precision) {
+  var s = goog.isDef(opt_precision) ? num.toFixed(opt_precision) : String(num);
+  var index = s.indexOf('.');
+  if (index == -1) {
+    index = s.length;
+  }
+  return goog.string.repeat('0', Math.max(0, length - index)) + s;
+};
+
+
+/**
+ * Returns a string representation of the given object, with
+ * null and undefined being returned as the empty string.
+ *
+ * @param {*} obj The object to convert.
+ * @return {string} A string representation of the {@code obj}.
+ */
+goog.string.makeSafe = function(obj) {
+  return obj == null ? '' : String(obj);
+};
+
+
+/**
+ * Concatenates string expressions. This is useful
+ * since some browsers are very inefficient when it comes to using plus to
+ * concat strings. Be careful when using null and undefined here since
+ * these will not be included in the result. If you need to represent these
+ * be sure to cast the argument to a String first.
+ * For example:
+ * <pre>buildString('a', 'b', 'c', 'd') -> 'abcd'
+ * buildString(null, undefined) -> ''
+ * </pre>
+ * @param {...*} var_args A list of strings to concatenate. If not a string,
+ *     it will be casted to one.
+ * @return {string} The concatenation of {@code var_args}.
+ */
+goog.string.buildString = function(var_args) {
+  return Array.prototype.join.call(arguments, '');
+};
+
+
+/**
+ * Returns a string with at least 64-bits of randomness.
+ *
+ * Doesn't trust Javascript's random function entirely. Uses a combination of
+ * random and current timestamp, and then encodes the string in base-36 to
+ * make it shorter.
+ *
+ * @return {string} A random string, e.g. sn1s7vb4gcic.
+ */
+goog.string.getRandomString = function() {
+  var x = 2147483648;
+  return Math.floor(Math.random() * x).toString(36) +
+      Math.abs(Math.floor(Math.random() * x) ^ goog.now()).toString(36);
+};
+
+
+/**
+ * Compares two version numbers.
+ *
+ * @param {string|number} version1 Version of first item.
+ * @param {string|number} version2 Version of second item.
+ *
+ * @return {number}  1 if {@code version1} is higher.
+ *                   0 if arguments are equal.
+ *                  -1 if {@code version2} is higher.
+ */
+goog.string.compareVersions = function(version1, version2) {
+  var order = 0;
+  // Trim leading and trailing whitespace and split the versions into
+  // subversions.
+  var v1Subs = goog.string.trim(String(version1)).split('.');
+  var v2Subs = goog.string.trim(String(version2)).split('.');
+  var subCount = Math.max(v1Subs.length, v2Subs.length);
+
+  // Iterate over the subversions, as long as they appear to be equivalent.
+  for (var subIdx = 0; order == 0 && subIdx < subCount; subIdx++) {
+    var v1Sub = v1Subs[subIdx] || '';
+    var v2Sub = v2Subs[subIdx] || '';
+
+    do {
+      // Split the subversions into pairs of numbers and qualifiers (like 'b').
+      // Two different RegExp objects are use to make it clear the code
+      // is side-effect free
+      var v1Comp = /(\d*)(\D*)(.*)/.exec(v1Sub) || ['', '', '', ''];
+      var v2Comp = /(\d*)(\D*)(.*)/.exec(v2Sub) || ['', '', '', ''];
+      // Break if there are no more matches.
+      if (v1Comp[0].length == 0 && v2Comp[0].length == 0) {
+        break;
+      }
+
+      // Parse the numeric part of the subversion. A missing number is
+      // equivalent to 0.
+      var v1CompNum = v1Comp[1].length == 0 ? 0 : parseInt(v1Comp[1], 10);
+      var v2CompNum = v2Comp[1].length == 0 ? 0 : parseInt(v2Comp[1], 10);
+
+      // Compare the subversion components. The number has the highest
+      // precedence. Next, if the numbers are equal, a subversion without any
+      // qualifier is always higher than a subversion with any qualifier. Next,
+      // the qualifiers are compared as strings.
+      order = goog.string.compareElements_(v1CompNum, v2CompNum) ||
+          goog.string.compareElements_(
+              v1Comp[2].length == 0, v2Comp[2].length == 0) ||
+          goog.string.compareElements_(v1Comp[2], v2Comp[2]);
+      // Stop as soon as an inequality is discovered.
+
+      v1Sub = v1Comp[3];
+      v2Sub = v2Comp[3];
+    } while (order == 0);
+  }
+
+  return order;
+};
+
+
+/**
+ * Compares elements of a version number.
+ *
+ * @param {string|number|boolean} left An element from a version number.
+ * @param {string|number|boolean} right An element from a version number.
+ *
+ * @return {number}  1 if {@code left} is higher.
+ *                   0 if arguments are equal.
+ *                  -1 if {@code right} is higher.
+ * @private
+ */
+goog.string.compareElements_ = function(left, right) {
+  if (left < right) {
+    return -1;
+  } else if (left > right) {
+    return 1;
+  }
+  return 0;
+};
+
+
+/**
+ * String hash function similar to java.lang.String.hashCode().
+ * The hash code for a string is computed as
+ * s[0] * 31 ^ (n - 1) + s[1] * 31 ^ (n - 2) + ... + s[n - 1],
+ * where s[i] is the ith character of the string and n is the length of
+ * the string. We mod the result to make it between 0 (inclusive) and 2^32
+ * (exclusive).
+ * @param {string} str A string.
+ * @return {number} Hash value for {@code str}, between 0 (inclusive) and 2^32
+ *  (exclusive). The empty string returns 0.
+ */
+goog.string.hashCode = function(str) {
+  var result = 0;
+  for (var i = 0; i < str.length; ++i) {
+    // Normalize to 4 byte range, 0 ... 2^32.
+    result = (31 * result + str.charCodeAt(i)) >>> 0;
+  }
+  return result;
+};
+
+
+/**
+ * The most recent unique ID. |0 is equivalent to Math.floor in this case.
+ * @type {number}
+ * @private
+ */
+goog.string.uniqueStringCounter_ = Math.random() * 0x80000000 | 0;
+
+
+/**
+ * Generates and returns a string which is unique in the current document.
+ * This is useful, for example, to create unique IDs for DOM elements.
+ * @return {string} A unique id.
+ */
+goog.string.createUniqueString = function() {
+  return 'goog_' + goog.string.uniqueStringCounter_++;
+};
+
+
+/**
+ * Converts the supplied string to a number, which may be Infinity or NaN.
+ * This function strips whitespace: (toNumber(' 123') === 123)
+ * This function accepts scientific notation: (toNumber('1e1') === 10)
+ *
+ * This is better than Javascript's built-in conversions because, sadly:
+ *     (Number(' ') === 0) and (parseFloat('123a') === 123)
+ *
+ * @param {string} str The string to convert.
+ * @return {number} The number the supplied string represents, or NaN.
+ */
+goog.string.toNumber = function(str) {
+  var num = Number(str);
+  if (num == 0 && goog.string.isEmptyOrWhitespace(str)) {
+    return NaN;
+  }
+  return num;
+};
+
+
+/**
+ * Returns whether the given string is lower camel case (e.g. "isFooBar").
+ *
+ * Note that this assumes the string is entirely letters.
+ * @see http://en.wikipedia.org/wiki/CamelCase#Variations_and_synonyms
+ *
+ * @param {string} str String to test.
+ * @return {boolean} Whether the string is lower camel case.
+ */
+goog.string.isLowerCamelCase = function(str) {
+  return /^[a-z]+([A-Z][a-z]*)*$/.test(str);
+};
+
+
+/**
+ * Returns whether the given string is upper camel case (e.g. "FooBarBaz").
+ *
+ * Note that this assumes the string is entirely letters.
+ * @see http://en.wikipedia.org/wiki/CamelCase#Variations_and_synonyms
+ *
+ * @param {string} str String to test.
+ * @return {boolean} Whether the string is upper camel case.
+ */
+goog.string.isUpperCamelCase = function(str) {
+  return /^([A-Z][a-z]*)+$/.test(str);
+};
+
+
+/**
+ * Converts a string from selector-case to camelCase (e.g. from
+ * "multi-part-string" to "multiPartString"), useful for converting
+ * CSS selectors and HTML dataset keys to their equivalent JS properties.
+ * @param {string} str The string in selector-case form.
+ * @return {string} The string in camelCase form.
+ */
+goog.string.toCamelCase = function(str) {
+  return String(str).replace(
+      /\-([a-z])/g, function(all, match) { return match.toUpperCase(); });
+};
+
+
+/**
+ * Converts a string from camelCase to selector-case (e.g. from
+ * "multiPartString" to "multi-part-string"), useful for converting JS
+ * style and dataset properties to equivalent CSS selectors and HTML keys.
+ * @param {string} str The string in camelCase form.
+ * @return {string} The string in selector-case form.
+ */
+goog.string.toSelectorCase = function(str) {
+  return String(str).replace(/([A-Z])/g, '-$1').toLowerCase();
+};
+
+
+/**
+ * Converts a string into TitleCase. First character of the string is always
+ * capitalized in addition to the first letter of every subsequent word.
+ * Words are delimited by one or more whitespaces by default. Custom delimiters
+ * can optionally be specified to replace the default, which doesn't preserve
+ * whitespace delimiters and instead must be explicitly included if needed.
+ *
+ * Default delimiter => " ":
+ *    goog.string.toTitleCase('oneTwoThree')    => 'OneTwoThree'
+ *    goog.string.toTitleCase('one two three')  => 'One Two Three'
+ *    goog.string.toTitleCase('  one   two   ') => '  One   Two   '
+ *    goog.string.toTitleCase('one_two_three')  => 'One_two_three'
+ *    goog.string.toTitleCase('one-two-three')  => 'One-two-three'
+ *
+ * Custom delimiter => "_-.":
+ *    goog.string.toTitleCase('oneTwoThree', '_-.')       => 'OneTwoThree'
+ *    goog.string.toTitleCase('one two three', '_-.')     => 'One two three'
+ *    goog.string.toTitleCase('  one   two   ', '_-.')    => '  one   two   '
+ *    goog.string.toTitleCase('one_two_three', '_-.')     => 'One_Two_Three'
+ *    goog.string.toTitleCase('one-two-three', '_-.')     => 'One-Two-Three'
+ *    goog.string.toTitleCase('one...two...three', '_-.') => 'One...Two...Three'
+ *    goog.string.toTitleCase('one. two. three', '_-.')   => 'One. two. three'
+ *    goog.string.toTitleCase('one-two.three', '_-.')     => 'One-Two.Three'
+ *
+ * @param {string} str String value in camelCase form.
+ * @param {string=} opt_delimiters Custom delimiter character set used to
+ *      distinguish words in the string value. Each character represents a
+ *      single delimiter. When provided, default whitespace delimiter is
+ *      overridden and must be explicitly included if needed.
+ * @return {string} String value in TitleCase form.
+ */
+goog.string.toTitleCase = function(str, opt_delimiters) {
+  var delimiters = goog.isString(opt_delimiters) ?
+      goog.string.regExpEscape(opt_delimiters) :
+      '\\s';
+
+  // For IE8, we need to prevent using an empty character set. Otherwise,
+  // incorrect matching will occur.
+  delimiters = delimiters ? '|[' + delimiters + ']+' : '';
+
+  var regexp = new RegExp('(^' + delimiters + ')([a-z])', 'g');
+  return str.replace(
+      regexp, function(all, p1, p2) { return p1 + p2.toUpperCase(); });
+};
+
+
+/**
+ * Capitalizes a string, i.e. converts the first letter to uppercase
+ * and all other letters to lowercase, e.g.:
+ *
+ * goog.string.capitalize('one')     => 'One'
+ * goog.string.capitalize('ONE')     => 'One'
+ * goog.string.capitalize('one two') => 'One two'
+ *
+ * Note that this function does not trim initial whitespace.
+ *
+ * @param {string} str String value to capitalize.
+ * @return {string} String value with first letter in uppercase.
+ */
+goog.string.capitalize = function(str) {
+  return String(str.charAt(0)).toUpperCase() +
+      String(str.substr(1)).toLowerCase();
+};
+
+
+/**
+ * Parse a string in decimal or hexidecimal ('0xFFFF') form.
+ *
+ * To parse a particular radix, please use parseInt(string, radix) directly. See
+ * https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/parseInt
+ *
+ * This is a wrapper for the built-in parseInt function that will only parse
+ * numbers as base 10 or base 16.  Some JS implementations assume strings
+ * starting with "0" are intended to be octal. ES3 allowed but discouraged
+ * this behavior. ES5 forbids it.  This function emulates the ES5 behavior.
+ *
+ * For more information, see Mozilla JS Reference: http://goo.gl/8RiFj
+ *
+ * @param {string|number|null|undefined} value The value to be parsed.
+ * @return {number} The number, parsed. If the string failed to parse, this
+ *     will be NaN.
+ */
+goog.string.parseInt = function(value) {
+  // Force finite numbers to strings.
+  if (isFinite(value)) {
+    value = String(value);
+  }
+
+  if (goog.isString(value)) {
+    // If the string starts with '0x' or '-0x', parse as hex.
+    return /^\s*-?0x/i.test(value) ? parseInt(value, 16) : parseInt(value, 10);
+  }
+
+  return NaN;
+};
+
+
+/**
+ * Splits a string on a separator a limited number of times.
+ *
+ * This implementation is more similar to Python or Java, where the limit
+ * parameter specifies the maximum number of splits rather than truncating
+ * the number of results.
+ *
+ * See http://docs.python.org/2/library/stdtypes.html#str.split
+ * See JavaDoc: http://goo.gl/F2AsY
+ * See Mozilla reference: http://goo.gl/dZdZs
+ *
+ * @param {string} str String to split.
+ * @param {string} separator The separator.
+ * @param {number} limit The limit to the number of splits. The resulting array
+ *     will have a maximum length of limit+1.  Negative numbers are the same
+ *     as zero.
+ * @return {!Array<string>} The string, split.
+ */
+goog.string.splitLimit = function(str, separator, limit) {
+  var parts = str.split(separator);
+  var returnVal = [];
+
+  // Only continue doing this while we haven't hit the limit and we have
+  // parts left.
+  while (limit > 0 && parts.length) {
+    returnVal.push(parts.shift());
+    limit--;
+  }
+
+  // If there are remaining parts, append them to the end.
+  if (parts.length) {
+    returnVal.push(parts.join(separator));
+  }
+
+  return returnVal;
+};
+
+
+/**
+ * Finds the characters to the right of the last instance of any separator
+ *
+ * This function is similar to goog.string.path.baseName, except it can take a
+ * list of characters to split the string on. It will return the rightmost
+ * grouping of characters to the right of any separator as a left-to-right
+ * oriented string.
+ *
+ * @see goog.string.path.baseName
+ * @param {string} str The string
+ * @param {string|!Array<string>} separators A list of separator characters
+ * @return {string} The last part of the string with respect to the separators
+ */
+goog.string.lastComponent = function(str, separators) {
+  if (!separators) {
+    return str;
+  } else if (typeof separators == 'string') {
+    separators = [separators];
+  }
+
+  var lastSeparatorIndex = -1;
+  for (var i = 0; i < separators.length; i++) {
+    if (separators[i] == '') {
+      continue;
+    }
+    var currentSeparatorIndex = str.lastIndexOf(separators[i]);
+    if (currentSeparatorIndex > lastSeparatorIndex) {
+      lastSeparatorIndex = currentSeparatorIndex;
+    }
+  }
+  if (lastSeparatorIndex == -1) {
+    return str;
+  }
+  return str.slice(lastSeparatorIndex + 1);
+};
+
+
+/**
+ * Computes the Levenshtein edit distance between two strings.
+ * @param {string} a
+ * @param {string} b
+ * @return {number} The edit distance between the two strings.
+ */
+goog.string.editDistance = function(a, b) {
+  var v0 = [];
+  var v1 = [];
+
+  if (a == b) {
+    return 0;
+  }
+
+  if (!a.length || !b.length) {
+    return Math.max(a.length, b.length);
+  }
+
+  for (var i = 0; i < b.length + 1; i++) {
+    v0[i] = i;
+  }
+
+  for (var i = 0; i < a.length; i++) {
+    v1[0] = i + 1;
+
+    for (var j = 0; j < b.length; j++) {
+      var cost = Number(a[i] != b[j]);
+      // Cost for the substring is the minimum of adding one character, removing
+      // one character, or a swap.
+      v1[j + 1] = Math.min(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost);
+    }
+
+    for (var j = 0; j < v0.length; j++) {
+      v0[j] = v1[j];
+    }
+  }
+
+  return v1[b.length];
+};
diff --git a/third_party/ink/closure/string/typedstring.js b/third_party/ink/closure/string/typedstring.js
new file mode 100644
index 0000000..d0d7bd9
--- /dev/null
+++ b/third_party/ink/closure/string/typedstring.js
@@ -0,0 +1,48 @@
+// Copyright 2013 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.
+
+goog.provide('goog.string.TypedString');
+
+
+
+/**
+ * Wrapper for strings that conform to a data type or language.
+ *
+ * Implementations of this interface are wrappers for strings, and typically
+ * associate a type contract with the wrapped string.  Concrete implementations
+ * of this interface may choose to implement additional run-time type checking,
+ * see for example {@code goog.html.SafeHtml}. If available, client code that
+ * needs to ensure type membership of an object should use the type's function
+ * to assert type membership, such as {@code goog.html.SafeHtml.unwrap}.
+ * @interface
+ */
+goog.string.TypedString = function() {};
+
+
+/**
+ * Interface marker of the TypedString interface.
+ *
+ * This property can be used to determine at runtime whether or not an object
+ * implements this interface.  All implementations of this interface set this
+ * property to {@code true}.
+ * @type {boolean}
+ */
+goog.string.TypedString.prototype.implementsGoogStringTypedString;
+
+
+/**
+ * Retrieves this wrapped string's value.
+ * @return {string} The wrapped string's value.
+ */
+goog.string.TypedString.prototype.getTypedStringValue;
diff --git a/third_party/ink/closure/structs/collection.js b/third_party/ink/closure/structs/collection.js
new file mode 100644
index 0000000..267862c
--- /dev/null
+++ b/third_party/ink/closure/structs/collection.js
@@ -0,0 +1,55 @@
+// 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 Defines the collection interface.
+ *
+ * @author nnaze@google.com (Nathan Naze)
+ */
+
+goog.provide('goog.structs.Collection');
+
+
+
+/**
+ * An interface for a collection of values.
+ * @interface
+ * @template T
+ */
+goog.structs.Collection = function() {};
+
+
+/**
+ * @param {T} value Value to add to the collection.
+ */
+goog.structs.Collection.prototype.add;
+
+
+/**
+ * @param {T} value Value to remove from the collection.
+ */
+goog.structs.Collection.prototype.remove;
+
+
+/**
+ * @param {T} value Value to find in the collection.
+ * @return {boolean} Whether the collection contains the specified value.
+ */
+goog.structs.Collection.prototype.contains;
+
+
+/**
+ * @return {number} The number of values stored in the collection.
+ */
+goog.structs.Collection.prototype.getCount;
diff --git a/third_party/ink/closure/structs/inversionmap.js b/third_party/ink/closure/structs/inversionmap.js
new file mode 100644
index 0000000..009c02aa
--- /dev/null
+++ b/third_party/ink/closure/structs/inversionmap.js
@@ -0,0 +1,158 @@
+// Copyright 2008 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 Provides inversion and inversion map functionality for storing
+ * integer ranges and corresponding values.
+ *
+ * @author cibu@google.com (Cibu Johny)
+ * @author markdavis@google.com (Mark Davis)
+ */
+
+goog.provide('goog.structs.InversionMap');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+
+
+
+/**
+ * Maps ranges to values.
+ * @param {Array<number>} rangeArray An array of monotonically
+ *     increasing integer values, with at least one instance.
+ * @param {Array<T>} valueArray An array of corresponding values.
+ *     Length must be the same as rangeArray.
+ * @param {boolean=} opt_delta If true, saves only delta from previous value.
+ * @constructor
+ * @template T
+ */
+goog.structs.InversionMap = function(rangeArray, valueArray, opt_delta) {
+  /**
+   * @protected {Array<number>}
+   */
+  this.rangeArray = null;
+
+  goog.asserts.assert(
+      rangeArray.length == valueArray.length,
+      'rangeArray and valueArray must have the same length.');
+  this.storeInversion_(rangeArray, opt_delta);
+
+  /** @protected {Array<T>} */
+  this.values = valueArray;
+};
+
+
+/**
+ * Stores the integers as ranges (half-open).
+ * If delta is true, the integers are delta from the previous value and
+ * will be restored to the absolute value.
+ * When used as a set, even indices are IN, and odd are OUT.
+ * @param {Array<number>} rangeArray An array of monotonically
+ *     increasing integer values, with at least one instance.
+ * @param {boolean=} opt_delta If true, saves only delta from previous value.
+ * @private
+ */
+goog.structs.InversionMap.prototype.storeInversion_ = function(
+    rangeArray, opt_delta) {
+  this.rangeArray = rangeArray;
+
+  for (var i = 1; i < rangeArray.length; i++) {
+    if (rangeArray[i] == null) {
+      rangeArray[i] = rangeArray[i - 1] + 1;
+    } else if (opt_delta) {
+      rangeArray[i] += rangeArray[i - 1];
+    }
+  }
+};
+
+
+/**
+ * Splices a range -> value map into this inversion map.
+ * @param {Array<number>} rangeArray An array of monotonically
+ *     increasing integer values, with at least one instance.
+ * @param {Array<T>} valueArray An array of corresponding values.
+ *     Length must be the same as rangeArray.
+ * @param {boolean=} opt_delta If true, saves only delta from previous value.
+ */
+goog.structs.InversionMap.prototype.spliceInversion = function(
+    rangeArray, valueArray, opt_delta) {
+  // By building another inversion map, we build the arrays that we need
+  // to splice in.
+  var otherMap =
+      new goog.structs.InversionMap(rangeArray, valueArray, opt_delta);
+
+  // Figure out where to splice those arrays.
+  var startRange = otherMap.rangeArray[0];
+  var endRange =
+      /** @type {number} */ (goog.array.peek(otherMap.rangeArray));
+  var startSplice = this.getLeast(startRange);
+  var endSplice = this.getLeast(endRange);
+
+  // The inversion map works by storing the start points of ranges...
+  if (startRange != this.rangeArray[startSplice]) {
+    // ...if we're splicing in a start point that isn't already here,
+    // then we need to insert it after the insertion point.
+    startSplice++;
+  }  // otherwise we overwrite the insertion point.
+
+  this.rangeArray = this.rangeArray.slice(0, startSplice)
+                        .concat(otherMap.rangeArray)
+                        .concat(this.rangeArray.slice(endSplice + 1));
+  this.values = this.values.slice(0, startSplice)
+                    .concat(otherMap.values)
+                    .concat(this.values.slice(endSplice + 1));
+};
+
+
+/**
+ * Gets the value corresponding to a number from the inversion map.
+ * @param {number} intKey The number for which value needs to be retrieved
+ *     from inversion map.
+ * @return {T|null} Value retrieved from inversion map; null if not found.
+ */
+goog.structs.InversionMap.prototype.at = function(intKey) {
+  var index = this.getLeast(intKey);
+  if (index < 0) {
+    return null;
+  }
+  return this.values[index];
+};
+
+
+/**
+ * Gets the largest index such that rangeArray[index] <= intKey from the
+ * inversion map.
+ * @param {number} intKey The probe for which rangeArray is searched.
+ * @return {number} Largest index such that rangeArray[index] <= intKey.
+ * @protected
+ */
+goog.structs.InversionMap.prototype.getLeast = function(intKey) {
+  var arr = this.rangeArray;
+  var low = 0;
+  var high = arr.length;
+  while (high - low > 8) {
+    var mid = (high + low) >> 1;
+    if (arr[mid] <= intKey) {
+      low = mid;
+    } else {
+      high = mid;
+    }
+  }
+  for (; low < high; ++low) {
+    if (intKey < arr[low]) {
+      break;
+    }
+  }
+  return low - 1;
+};
diff --git a/third_party/ink/closure/structs/map.js b/third_party/ink/closure/structs/map.js
new file mode 100644
index 0000000..ccc8184
--- /dev/null
+++ b/third_party/ink/closure/structs/map.js
@@ -0,0 +1,485 @@
+// Copyright 2006 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 Datastructure: Hash Map.
+ *
+ * @author arv@google.com (Erik Arvidsson)
+ * @author jonp@google.com (Jon Perlow) Optimized for IE6
+ *
+ * This file contains an implementation of a Map structure. It implements a lot
+ * of the methods used in goog.structs so those functions work on hashes. This
+ * is best suited for complex key types. For simple keys such as numbers and
+ * strings consider using the lighter-weight utilities in goog.object.
+ * MOE:begin_intracomment_strip
+ *
+ * NOTE(flan): Internally, key types are NOT actually cast to
+ * strings. Some people actually rely on this behavior even though it
+ * is incorrect. For more information, see http://b/5622311.
+ *
+ * NOTE(flan): Erik Corry (erikcorry) from the V8 team went over this
+ * class with me to help look for simplifications and
+ * optimizations. In the end, he didn't come up with very much. Erik
+ * explained that "for (k in o)" is not optimized in Crankshaft
+ * because it needs to look up properties in the whole prototype
+ * chain. It also needs to return the keys in order. Thus keeping an
+ * array of keys is actually much more efficient.
+ *
+ * Likewise, one option to iterate safely with "for (k in o)" is to
+ * prefix the keys with some character, like ':'. This can create a
+ * lot of strings that didn't exist before. In Closure Labs,
+ * goog.labs.structs.Map uses extra arrays to store non-safe keys and
+ * values.
+ *
+ * Thus, there are not a lot of reasonable simplifications that can be
+ * done here without impacting performance.
+ *
+ * TODO(chrishenry): Create some performance benchmarks for common
+ * operations.
+ * MOE:end_intracomment_strip
+ */
+
+
+goog.provide('goog.structs.Map');
+
+goog.require('goog.iter.Iterator');
+goog.require('goog.iter.StopIteration');
+goog.require('goog.object');
+
+
+
+/**
+ * Class for Hash Map datastructure.
+ * @param {*=} opt_map Map or Object to initialize the map with.
+ * @param {...*} var_args If 2 or more arguments are present then they
+ *     will be used as key-value pairs.
+ * @constructor
+ * @template K, V
+ * @deprecated This type is misleading: use ES6 Map instead.
+ */
+goog.structs.Map = function(opt_map, var_args) {
+
+  /**
+   * Underlying JS object used to implement the map.
+   * @private {!Object}
+   */
+  this.map_ = {};
+
+  /**
+   * An array of keys. This is necessary for two reasons:
+   *   1. Iterating the keys using for (var key in this.map_) allocates an
+   *      object for every key in IE which is really bad for IE6 GC perf.
+   *   2. Without a side data structure, we would need to escape all the keys
+   *      as that would be the only way we could tell during iteration if the
+   *      key was an internal key or a property of the object.
+   *
+   * This array can contain deleted keys so it's necessary to check the map
+   * as well to see if the key is still in the map (this doesn't require a
+   * memory allocation in IE).
+   * @private {!Array<string>}
+   */
+  this.keys_ = [];
+
+  /**
+   * The number of key value pairs in the map.
+   * @private {number}
+   */
+  this.count_ = 0;
+
+  /**
+   * Version used to detect changes while iterating.
+   * @private {number}
+   */
+  this.version_ = 0;
+
+  var argLength = arguments.length;
+
+  if (argLength > 1) {
+    if (argLength % 2) {
+      throw new Error('Uneven number of arguments');
+    }
+    for (var i = 0; i < argLength; i += 2) {
+      this.set(arguments[i], arguments[i + 1]);
+    }
+  } else if (opt_map) {
+    this.addAll(/** @type {Object} */ (opt_map));
+  }
+};
+
+
+/**
+ * @return {number} The number of key-value pairs in the map.
+ */
+goog.structs.Map.prototype.getCount = function() {
+  return this.count_;
+};
+
+
+/**
+ * Returns the values of the map.
+ * @return {!Array<V>} The values in the map.
+ */
+goog.structs.Map.prototype.getValues = function() {
+  this.cleanupKeysArray_();
+
+  var rv = [];
+  for (var i = 0; i < this.keys_.length; i++) {
+    var key = this.keys_[i];
+    rv.push(this.map_[key]);
+  }
+  return rv;
+};
+
+
+/**
+ * Returns the keys of the map.
+ * @return {!Array<string>} Array of string values.
+ */
+goog.structs.Map.prototype.getKeys = function() {
+  this.cleanupKeysArray_();
+  return /** @type {!Array<string>} */ (this.keys_.concat());
+};
+
+
+/**
+ * Whether the map contains the given key.
+ * @param {*} key The key to check for.
+ * @return {boolean} Whether the map contains the key.
+ */
+goog.structs.Map.prototype.containsKey = function(key) {
+  return goog.structs.Map.hasKey_(this.map_, key);
+};
+
+
+/**
+ * Whether the map contains the given value. This is O(n).
+ * @param {V} val The value to check for.
+ * @return {boolean} Whether the map contains the value.
+ */
+goog.structs.Map.prototype.containsValue = function(val) {
+  for (var i = 0; i < this.keys_.length; i++) {
+    var key = this.keys_[i];
+    if (goog.structs.Map.hasKey_(this.map_, key) && this.map_[key] == val) {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Whether this map is equal to the argument map.
+ * @param {goog.structs.Map} otherMap The map against which to test equality.
+ * @param {function(V, V): boolean=} opt_equalityFn Optional equality function
+ *     to test equality of values. If not specified, this will test whether
+ *     the values contained in each map are identical objects.
+ * @return {boolean} Whether the maps are equal.
+ */
+goog.structs.Map.prototype.equals = function(otherMap, opt_equalityFn) {
+  if (this === otherMap) {
+    return true;
+  }
+
+  if (this.count_ != otherMap.getCount()) {
+    return false;
+  }
+
+  var equalityFn = opt_equalityFn || goog.structs.Map.defaultEquals;
+
+  this.cleanupKeysArray_();
+  for (var key, i = 0; key = this.keys_[i]; i++) {
+    if (!equalityFn(this.get(key), otherMap.get(key))) {
+      return false;
+    }
+  }
+
+  return true;
+};
+
+
+/**
+ * Default equality test for values.
+ * @param {*} a The first value.
+ * @param {*} b The second value.
+ * @return {boolean} Whether a and b reference the same object.
+ */
+goog.structs.Map.defaultEquals = function(a, b) {
+  return a === b;
+};
+
+
+/**
+ * @return {boolean} Whether the map is empty.
+ */
+goog.structs.Map.prototype.isEmpty = function() {
+  return this.count_ == 0;
+};
+
+
+/**
+ * Removes all key-value pairs from the map.
+ */
+goog.structs.Map.prototype.clear = function() {
+  this.map_ = {};
+  this.keys_.length = 0;
+  this.count_ = 0;
+  this.version_ = 0;
+};
+
+
+/**
+ * Removes a key-value pair based on the key. This is O(logN) amortized due to
+ * updating the keys array whenever the count becomes half the size of the keys
+ * in the keys array.
+ * @param {*} key  The key to remove.
+ * @return {boolean} Whether object was removed.
+ */
+goog.structs.Map.prototype.remove = function(key) {
+  if (goog.structs.Map.hasKey_(this.map_, key)) {
+    delete this.map_[key];
+    this.count_--;
+    this.version_++;
+
+    // clean up the keys array if the threshold is hit
+    if (this.keys_.length > 2 * this.count_) {
+      this.cleanupKeysArray_();
+    }
+
+    return true;
+  }
+  return false;
+};
+
+
+/**
+ * Cleans up the temp keys array by removing entries that are no longer in the
+ * map.
+ * @private
+ */
+goog.structs.Map.prototype.cleanupKeysArray_ = function() {
+  if (this.count_ != this.keys_.length) {
+    // First remove keys that are no longer in the map.
+    var srcIndex = 0;
+    var destIndex = 0;
+    while (srcIndex < this.keys_.length) {
+      var key = this.keys_[srcIndex];
+      if (goog.structs.Map.hasKey_(this.map_, key)) {
+        this.keys_[destIndex++] = key;
+      }
+      srcIndex++;
+    }
+    this.keys_.length = destIndex;
+  }
+
+  if (this.count_ != this.keys_.length) {
+    // If the count still isn't correct, that means we have duplicates. This can
+    // happen when the same key is added and removed multiple times. Now we have
+    // to allocate one extra Object to remove the duplicates. This could have
+    // been done in the first pass, but in the common case, we can avoid
+    // allocating an extra object by only doing this when necessary.
+    var seen = {};
+    var srcIndex = 0;
+    var destIndex = 0;
+    while (srcIndex < this.keys_.length) {
+      var key = this.keys_[srcIndex];
+      if (!(goog.structs.Map.hasKey_(seen, key))) {
+        this.keys_[destIndex++] = key;
+        seen[key] = 1;
+      }
+      srcIndex++;
+    }
+    this.keys_.length = destIndex;
+  }
+};
+
+
+/**
+ * Returns the value for the given key.  If the key is not found and the default
+ * value is not given this will return {@code undefined}.
+ * @param {*} key The key to get the value for.
+ * @param {DEFAULT=} opt_val The value to return if no item is found for the
+ *     given key, defaults to undefined.
+ * @return {V|DEFAULT} The value for the given key.
+ * @template DEFAULT
+ */
+goog.structs.Map.prototype.get = function(key, opt_val) {
+  if (goog.structs.Map.hasKey_(this.map_, key)) {
+    return this.map_[key];
+  }
+  return opt_val;
+};
+
+
+/**
+ * Adds a key-value pair to the map.
+ * @param {*} key The key.
+ * @param {V} value The value to add.
+ * @return {*} Some subclasses return a value.
+ */
+goog.structs.Map.prototype.set = function(key, value) {
+  if (!(goog.structs.Map.hasKey_(this.map_, key))) {
+    this.count_++;
+    // TODO(johnlenz): This class lies, it claims to return an array of string
+    // keys, but instead returns the original object used.
+    this.keys_.push(/** @type {?} */ (key));
+    // Only change the version if we add a new key.
+    this.version_++;
+  }
+  this.map_[key] = value;
+};
+
+
+/**
+ * Adds multiple key-value pairs from another goog.structs.Map or Object.
+ * @param {Object} map  Object containing the data to add.
+ */
+goog.structs.Map.prototype.addAll = function(map) {
+  var keys, values;
+  if (map instanceof goog.structs.Map) {
+    keys = map.getKeys();
+    values = map.getValues();
+  } else {
+    keys = goog.object.getKeys(map);
+    values = goog.object.getValues(map);
+  }
+  // we could use goog.array.forEach here but I don't want to introduce that
+  // dependency just for this.
+  for (var i = 0; i < keys.length; i++) {
+    this.set(keys[i], values[i]);
+  }
+};
+
+
+/**
+ * Calls the given function on each entry in the map.
+ * @param {function(this:T, V, K, goog.structs.Map<K,V>)} f
+ * @param {T=} opt_obj The value of "this" inside f.
+ * @template T
+ */
+goog.structs.Map.prototype.forEach = function(f, opt_obj) {
+  var keys = this.getKeys();
+  for (var i = 0; i < keys.length; i++) {
+    var key = keys[i];
+    var value = this.get(key);
+    f.call(opt_obj, value, key, this);
+  }
+};
+
+
+/**
+ * Clones a map and returns a new map.
+ * @return {!goog.structs.Map} A new map with the same key-value pairs.
+ */
+goog.structs.Map.prototype.clone = function() {
+  return new goog.structs.Map(this);
+};
+
+
+/**
+ * Returns a new map in which all the keys and values are interchanged
+ * (keys become values and values become keys). If multiple keys map to the
+ * same value, the chosen transposed value is implementation-dependent.
+ *
+ * It acts very similarly to {goog.object.transpose(Object)}.
+ *
+ * @return {!goog.structs.Map} The transposed map.
+ */
+goog.structs.Map.prototype.transpose = function() {
+  var transposed = new goog.structs.Map();
+  for (var i = 0; i < this.keys_.length; i++) {
+    var key = this.keys_[i];
+    var value = this.map_[key];
+    transposed.set(value, key);
+  }
+
+  return transposed;
+};
+
+
+/**
+ * @return {!Object} Object representation of the map.
+ */
+goog.structs.Map.prototype.toObject = function() {
+  this.cleanupKeysArray_();
+  var obj = {};
+  for (var i = 0; i < this.keys_.length; i++) {
+    var key = this.keys_[i];
+    obj[key] = this.map_[key];
+  }
+  return obj;
+};
+
+
+/**
+ * Returns an iterator that iterates over the keys in the map.  Removal of keys
+ * while iterating might have undesired side effects.
+ * @return {!goog.iter.Iterator} An iterator over the keys in the map.
+ */
+goog.structs.Map.prototype.getKeyIterator = function() {
+  return this.__iterator__(true);
+};
+
+
+/**
+ * Returns an iterator that iterates over the values in the map.  Removal of
+ * keys while iterating might have undesired side effects.
+ * @return {!goog.iter.Iterator} An iterator over the values in the map.
+ */
+goog.structs.Map.prototype.getValueIterator = function() {
+  return this.__iterator__(false);
+};
+
+
+/**
+ * Returns an iterator that iterates over the values or the keys in the map.
+ * This throws an exception if the map was mutated since the iterator was
+ * created.
+ * @param {boolean=} opt_keys True to iterate over the keys. False to iterate
+ *     over the values.  The default value is false.
+ * @return {!goog.iter.Iterator} An iterator over the values or keys in the map.
+ */
+goog.structs.Map.prototype.__iterator__ = function(opt_keys) {
+  // Clean up keys to minimize the risk of iterating over dead keys.
+  this.cleanupKeysArray_();
+
+  var i = 0;
+  var version = this.version_;
+  var selfObj = this;
+
+  var newIter = new goog.iter.Iterator;
+  newIter.next = function() {
+    if (version != selfObj.version_) {
+      throw new Error('The map has changed since the iterator was created');
+    }
+    if (i >= selfObj.keys_.length) {
+      throw goog.iter.StopIteration;
+    }
+    var key = selfObj.keys_[i++];
+    return opt_keys ? key : selfObj.map_[key];
+  };
+  return newIter;
+};
+
+
+/**
+ * Safe way to test for hasOwnProperty.  It even allows testing for
+ * 'hasOwnProperty'.
+ * @param {Object} obj The object to test for presence of the given key.
+ * @param {*} key The key to check for.
+ * @return {boolean} Whether the object has the key.
+ * @private
+ */
+goog.structs.Map.hasKey_ = function(obj, key) {
+  return Object.prototype.hasOwnProperty.call(obj, key);
+};
diff --git a/third_party/ink/closure/structs/set.js b/third_party/ink/closure/structs/set.js
new file mode 100644
index 0000000..e8470ab
--- /dev/null
+++ b/third_party/ink/closure/structs/set.js
@@ -0,0 +1,280 @@
+// Copyright 2006 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 Datastructure: Set.
+ *
+ * @author arv@google.com (Erik Arvidsson)
+ * @author pallosp@google.com (Peter Pallos)
+ *
+ * This class implements a set data structure. Adding and removing is O(1). It
+ * supports both object and primitive values. Be careful because you can add
+ * both 1 and new Number(1), because these are not the same. You can even add
+ * multiple new Number(1) because these are not equal.
+ */
+
+
+goog.provide('goog.structs.Set');
+
+goog.require('goog.structs');
+goog.require('goog.structs.Collection');
+goog.require('goog.structs.Map');
+
+
+
+/**
+ * A set that can contain both primitives and objects.  Adding and removing
+ * elements is O(1).  Primitives are treated as identical if they have the same
+ * type and convert to the same string.  Objects are treated as identical only
+ * if they are references to the same object.  WARNING: A goog.structs.Set can
+ * contain both 1 and (new Number(1)), because they are not the same.  WARNING:
+ * Adding (new Number(1)) twice will yield two distinct elements, because they
+ * are two different objects.  WARNING: Any object that is added to a
+ * goog.structs.Set will be modified!  Because goog.getUid() is used to
+ * identify objects, every object in the set will be mutated.
+ * @param {Array<T>|Object<?,T>=} opt_values Initial values to start with.
+ * @constructor
+ * @implements {goog.structs.Collection<T>}
+ * @final
+ * @template T
+ * @deprecated This type is misleading: use ES6 Set instead.
+ */
+goog.structs.Set = function(opt_values) {
+  this.map_ = new goog.structs.Map;
+  if (opt_values) {
+    this.addAll(opt_values);
+  }
+};
+
+
+/**
+ * Obtains a unique key for an element of the set.  Primitives will yield the
+ * same key if they have the same type and convert to the same string.  Object
+ * references will yield the same key only if they refer to the same object.
+ * @param {*} val Object or primitive value to get a key for.
+ * @return {string} A unique key for this value/object.
+ * @private
+ */
+goog.structs.Set.getKey_ = function(val) {
+  var type = typeof val;
+  if (type == 'object' && val || type == 'function') {
+    return 'o' + goog.getUid(/** @type {Object} */ (val));
+  } else {
+    return type.substr(0, 1) + val;
+  }
+};
+
+
+/**
+ * @return {number} The number of elements in the set.
+ * @override
+ */
+goog.structs.Set.prototype.getCount = function() {
+  return this.map_.getCount();
+};
+
+
+/**
+ * Add a primitive or an object to the set.
+ * @param {T} element The primitive or object to add.
+ * @override
+ */
+goog.structs.Set.prototype.add = function(element) {
+  this.map_.set(goog.structs.Set.getKey_(element), element);
+};
+
+
+/**
+ * Adds all the values in the given collection to this set.
+ * @param {Array<T>|goog.structs.Collection<T>|Object<?,T>} col A collection
+ *     containing the elements to add.
+ */
+goog.structs.Set.prototype.addAll = function(col) {
+  var values = goog.structs.getValues(col);
+  var l = values.length;
+  for (var i = 0; i < l; i++) {
+    this.add(values[i]);
+  }
+};
+
+
+/**
+ * Removes all values in the given collection from this set.
+ * @param {Array<T>|goog.structs.Collection<T>|Object<?,T>} col A collection
+ *     containing the elements to remove.
+ */
+goog.structs.Set.prototype.removeAll = function(col) {
+  var values = goog.structs.getValues(col);
+  var l = values.length;
+  for (var i = 0; i < l; i++) {
+    this.remove(values[i]);
+  }
+};
+
+
+/**
+ * Removes the given element from this set.
+ * @param {T} element The primitive or object to remove.
+ * @return {boolean} Whether the element was found and removed.
+ * @override
+ */
+goog.structs.Set.prototype.remove = function(element) {
+  return this.map_.remove(goog.structs.Set.getKey_(element));
+};
+
+
+/**
+ * Removes all elements from this set.
+ */
+goog.structs.Set.prototype.clear = function() {
+  this.map_.clear();
+};
+
+
+/**
+ * Tests whether this set is empty.
+ * @return {boolean} True if there are no elements in this set.
+ */
+goog.structs.Set.prototype.isEmpty = function() {
+  return this.map_.isEmpty();
+};
+
+
+/**
+ * Tests whether this set contains the given element.
+ * @param {T} element The primitive or object to test for.
+ * @return {boolean} True if this set contains the given element.
+ * @override
+ */
+goog.structs.Set.prototype.contains = function(element) {
+  return this.map_.containsKey(goog.structs.Set.getKey_(element));
+};
+
+
+/**
+ * Tests whether this set contains all the values in a given collection.
+ * Repeated elements in the collection are ignored, e.g.  (new
+ * goog.structs.Set([1, 2])).containsAll([1, 1]) is True.
+ * @param {goog.structs.Collection<T>|Object} col A collection-like object.
+ * @return {boolean} True if the set contains all elements.
+ */
+goog.structs.Set.prototype.containsAll = function(col) {
+  return goog.structs.every(col, this.contains, this);
+};
+
+
+/**
+ * Finds all values that are present in both this set and the given collection.
+ * @param {Array<S>|Object<?,S>} col A collection.
+ * @return {!goog.structs.Set<T|S>} A new set containing all the values
+ *     (primitives or objects) present in both this set and the given
+ *     collection.
+ * @template S
+ */
+goog.structs.Set.prototype.intersection = function(col) {
+  var result = new goog.structs.Set();
+
+  var values = goog.structs.getValues(col);
+  for (var i = 0; i < values.length; i++) {
+    var value = values[i];
+    if (this.contains(value)) {
+      result.add(value);
+    }
+  }
+
+  return result;
+};
+
+
+/**
+ * Finds all values that are present in this set and not in the given
+ * collection.
+ * @param {Array<T>|goog.structs.Collection<T>|Object<?,T>} col A collection.
+ * @return {!goog.structs.Set} A new set containing all the values
+ *     (primitives or objects) present in this set but not in the given
+ *     collection.
+ */
+goog.structs.Set.prototype.difference = function(col) {
+  var result = this.clone();
+  result.removeAll(col);
+  return result;
+};
+
+
+/**
+ * Returns an array containing all the elements in this set.
+ * @return {!Array<T>} An array containing all the elements in this set.
+ */
+goog.structs.Set.prototype.getValues = function() {
+  return this.map_.getValues();
+};
+
+
+/**
+ * Creates a shallow clone of this set.
+ * @return {!goog.structs.Set<T>} A new set containing all the same elements as
+ *     this set.
+ */
+goog.structs.Set.prototype.clone = function() {
+  return new goog.structs.Set(this);
+};
+
+
+/**
+ * Tests whether the given collection consists of the same elements as this set,
+ * regardless of order, without repetition.  Primitives are treated as equal if
+ * they have the same type and convert to the same string; objects are treated
+ * as equal if they are references to the same object.  This operation is O(n).
+ * @param {goog.structs.Collection<T>|Object} col A collection.
+ * @return {boolean} True if the given collection consists of the same elements
+ *     as this set, regardless of order, without repetition.
+ */
+goog.structs.Set.prototype.equals = function(col) {
+  return this.getCount() == goog.structs.getCount(col) && this.isSubsetOf(col);
+};
+
+
+/**
+ * Tests whether the given collection contains all the elements in this set.
+ * Primitives are treated as equal if they have the same type and convert to the
+ * same string; objects are treated as equal if they are references to the same
+ * object.  This operation is O(n).
+ * @param {goog.structs.Collection<T>|Object} col A collection.
+ * @return {boolean} True if this set is a subset of the given collection.
+ */
+goog.structs.Set.prototype.isSubsetOf = function(col) {
+  var colCount = goog.structs.getCount(col);
+  if (this.getCount() > colCount) {
+    return false;
+  }
+  // TODO(pallosp) Find the minimal collection size where the conversion makes
+  // the contains() method faster.
+  if (!(col instanceof goog.structs.Set) && colCount > 5) {
+    // Convert to a goog.structs.Set so that goog.structs.contains runs in
+    // O(1) time instead of O(n) time.
+    col = new goog.structs.Set(col);
+  }
+  return goog.structs.every(
+      this, function(value) { return goog.structs.contains(col, value); });
+};
+
+
+/**
+ * Returns an iterator that iterates over the elements in this set.
+ * @param {boolean=} opt_keys This argument is ignored.
+ * @return {!goog.iter.Iterator} An iterator over the elements in this set.
+ */
+goog.structs.Set.prototype.__iterator__ = function(opt_keys) {
+  return this.map_.__iterator__(false);
+};
diff --git a/third_party/ink/closure/structs/structs.js b/third_party/ink/closure/structs/structs.js
new file mode 100644
index 0000000..684ddfe4
--- /dev/null
+++ b/third_party/ink/closure/structs/structs.js
@@ -0,0 +1,354 @@
+// Copyright 2006 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 Generics method for collection-like classes and objects.
+ *
+ * @author arv@google.com (Erik Arvidsson)
+ *
+ * This file contains functions to work with collections. It supports using
+ * Map, Set, Array and Object and other classes that implement collection-like
+ * methods.
+ */
+
+
+goog.provide('goog.structs');
+
+goog.require('goog.array');
+goog.require('goog.object');
+
+
+// We treat an object as a dictionary if it has getKeys or it is an object that
+// isn't arrayLike.
+
+
+/**
+ * Returns the number of values in the collection-like object.
+ * @param {Object} col The collection-like object.
+ * @return {number} The number of values in the collection-like object.
+ */
+goog.structs.getCount = function(col) {
+  if (col.getCount && typeof col.getCount == 'function') {
+    return col.getCount();
+  }
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    return col.length;
+  }
+  return goog.object.getCount(col);
+};
+
+
+/**
+ * Returns the values of the collection-like object.
+ * @param {Object} col The collection-like object.
+ * @return {!Array<?>} The values in the collection-like object.
+ */
+goog.structs.getValues = function(col) {
+  if (col.getValues && typeof col.getValues == 'function') {
+    return col.getValues();
+  }
+  if (goog.isString(col)) {
+    return col.split('');
+  }
+  if (goog.isArrayLike(col)) {
+    var rv = [];
+    var l = col.length;
+    for (var i = 0; i < l; i++) {
+      rv.push(col[i]);
+    }
+    return rv;
+  }
+  return goog.object.getValues(col);
+};
+
+
+/**
+ * Returns the keys of the collection. Some collections have no notion of
+ * keys/indexes and this function will return undefined in those cases.
+ * @param {Object} col The collection-like object.
+ * @return {!Array|undefined} The keys in the collection.
+ */
+goog.structs.getKeys = function(col) {
+  if (col.getKeys && typeof col.getKeys == 'function') {
+    return col.getKeys();
+  }
+  // if we have getValues but no getKeys we know this is a key-less collection
+  if (col.getValues && typeof col.getValues == 'function') {
+    return undefined;
+  }
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    var rv = [];
+    var l = col.length;
+    for (var i = 0; i < l; i++) {
+      rv.push(i);
+    }
+    return rv;
+  }
+
+  return goog.object.getKeys(col);
+};
+
+
+/**
+ * Whether the collection contains the given value. This is O(n) and uses
+ * equals (==) to test the existence.
+ * @param {Object} col The collection-like object.
+ * @param {*} val The value to check for.
+ * @return {boolean} True if the map contains the value.
+ */
+goog.structs.contains = function(col, val) {
+  if (col.contains && typeof col.contains == 'function') {
+    return col.contains(val);
+  }
+  if (col.containsValue && typeof col.containsValue == 'function') {
+    return col.containsValue(val);
+  }
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    return goog.array.contains(/** @type {!Array<?>} */ (col), val);
+  }
+  return goog.object.containsValue(col, val);
+};
+
+
+/**
+ * Whether the collection is empty.
+ * @param {Object} col The collection-like object.
+ * @return {boolean} True if empty.
+ */
+goog.structs.isEmpty = function(col) {
+  if (col.isEmpty && typeof col.isEmpty == 'function') {
+    return col.isEmpty();
+  }
+
+  // We do not use goog.string.isEmptyOrWhitespace because here we treat the
+  // string as
+  // collection and as such even whitespace matters
+
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    return goog.array.isEmpty(/** @type {!Array<?>} */ (col));
+  }
+  return goog.object.isEmpty(col);
+};
+
+
+/**
+ * Removes all the elements from the collection.
+ * @param {Object} col The collection-like object.
+ */
+goog.structs.clear = function(col) {
+  // NOTE(arv): This should not contain strings because strings are immutable
+  if (col.clear && typeof col.clear == 'function') {
+    col.clear();
+  } else if (goog.isArrayLike(col)) {
+    goog.array.clear(/** @type {IArrayLike<?>} */ (col));
+  } else {
+    goog.object.clear(col);
+  }
+};
+
+
+/**
+ * Calls a function for each value in a collection. The function takes
+ * three arguments; the value, the key and the collection.
+ *
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):?} f The function to call for every value.
+ *     This function takes
+ *     3 arguments (the value, the key or undefined if the collection has no
+ *     notion of keys, and the collection) and the return value is irrelevant.
+ * @param {T=} opt_obj The object to be used as the value of 'this'
+ *     within {@code f}.
+ * @template T,S
+ * @deprecated Use a more specific method, e.g. goog.array.forEach,
+ *     goog.object.forEach, or for-of.
+ */
+goog.structs.forEach = function(col, f, opt_obj) {
+  if (col.forEach && typeof col.forEach == 'function') {
+    col.forEach(f, opt_obj);
+  } else if (goog.isArrayLike(col) || goog.isString(col)) {
+    goog.array.forEach(/** @type {!Array<?>} */ (col), f, opt_obj);
+  } else {
+    var keys = goog.structs.getKeys(col);
+    var values = goog.structs.getValues(col);
+    var l = values.length;
+    for (var i = 0; i < l; i++) {
+      f.call(/** @type {?} */ (opt_obj), values[i], keys && keys[i], col);
+    }
+  }
+};
+
+
+/**
+ * Calls a function for every value in the collection. When a call returns true,
+ * adds the value to a new collection (Array is returned by default).
+ *
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):boolean} f The function to call for every
+ *     value. This function takes
+ *     3 arguments (the value, the key or undefined if the collection has no
+ *     notion of keys, and the collection) and should return a Boolean. If the
+ *     return value is true the value is added to the result collection. If it
+ *     is false the value is not included.
+ * @param {T=} opt_obj The object to be used as the value of 'this'
+ *     within {@code f}.
+ * @return {!Object|!Array<?>} A new collection where the passed values are
+ *     present. If col is a key-less collection an array is returned.  If col
+ *     has keys and values a plain old JS object is returned.
+ * @template T,S
+ */
+goog.structs.filter = function(col, f, opt_obj) {
+  if (typeof col.filter == 'function') {
+    return col.filter(f, opt_obj);
+  }
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    return goog.array.filter(/** @type {!Array<?>} */ (col), f, opt_obj);
+  }
+
+  var rv;
+  var keys = goog.structs.getKeys(col);
+  var values = goog.structs.getValues(col);
+  var l = values.length;
+  if (keys) {
+    rv = {};
+    for (var i = 0; i < l; i++) {
+      if (f.call(/** @type {?} */ (opt_obj), values[i], keys[i], col)) {
+        rv[keys[i]] = values[i];
+      }
+    }
+  } else {
+    // We should not use goog.array.filter here since we want to make sure that
+    // the index is undefined as well as make sure that col is passed to the
+    // function.
+    rv = [];
+    for (var i = 0; i < l; i++) {
+      if (f.call(opt_obj, values[i], undefined, col)) {
+        rv.push(values[i]);
+      }
+    }
+  }
+  return rv;
+};
+
+
+/**
+ * Calls a function for every value in the collection and adds the result into a
+ * new collection (defaults to creating a new Array).
+ *
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):V} f The function to call for every value.
+ *     This function takes 3 arguments (the value, the key or undefined if the
+ *     collection has no notion of keys, and the collection) and should return
+ *     something. The result will be used as the value in the new collection.
+ * @param {T=} opt_obj  The object to be used as the value of 'this'
+ *     within {@code f}.
+ * @return {!Object<V>|!Array<V>} A new collection with the new values.  If
+ *     col is a key-less collection an array is returned.  If col has keys and
+ *     values a plain old JS object is returned.
+ * @template T,S,V
+ */
+goog.structs.map = function(col, f, opt_obj) {
+  if (typeof col.map == 'function') {
+    return col.map(f, opt_obj);
+  }
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    return goog.array.map(/** @type {!Array<?>} */ (col), f, opt_obj);
+  }
+
+  var rv;
+  var keys = goog.structs.getKeys(col);
+  var values = goog.structs.getValues(col);
+  var l = values.length;
+  if (keys) {
+    rv = {};
+    for (var i = 0; i < l; i++) {
+      rv[keys[i]] = f.call(/** @type {?} */ (opt_obj), values[i], keys[i], col);
+    }
+  } else {
+    // We should not use goog.array.map here since we want to make sure that
+    // the index is undefined as well as make sure that col is passed to the
+    // function.
+    rv = [];
+    for (var i = 0; i < l; i++) {
+      rv[i] = f.call(/** @type {?} */ (opt_obj), values[i], undefined, col);
+    }
+  }
+  return rv;
+};
+
+
+/**
+ * Calls f for each value in a collection. If any call returns true this returns
+ * true (without checking the rest). If all returns false this returns false.
+ *
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):boolean} f The function to call for every
+ *     value. This function takes 3 arguments (the value, the key or undefined
+ *     if the collection has no notion of keys, and the collection) and should
+ *     return a boolean.
+ * @param {T=} opt_obj  The object to be used as the value of 'this'
+ *     within {@code f}.
+ * @return {boolean} True if any value passes the test.
+ * @template T,S
+ */
+goog.structs.some = function(col, f, opt_obj) {
+  if (typeof col.some == 'function') {
+    return col.some(f, opt_obj);
+  }
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    return goog.array.some(/** @type {!Array<?>} */ (col), f, opt_obj);
+  }
+  var keys = goog.structs.getKeys(col);
+  var values = goog.structs.getValues(col);
+  var l = values.length;
+  for (var i = 0; i < l; i++) {
+    if (f.call(/** @type {?} */ (opt_obj), values[i], keys && keys[i], col)) {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Calls f for each value in a collection. If all calls return true this return
+ * true this returns true. If any returns false this returns false at this point
+ *  and does not continue to check the remaining values.
+ *
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):boolean} f The function to call for every
+ *     value. This function takes 3 arguments (the value, the key or
+ *     undefined if the collection has no notion of keys, and the collection)
+ *     and should return a boolean.
+ * @param {T=} opt_obj  The object to be used as the value of 'this'
+ *     within {@code f}.
+ * @return {boolean} True if all key-value pairs pass the test.
+ * @template T,S
+ */
+goog.structs.every = function(col, f, opt_obj) {
+  if (typeof col.every == 'function') {
+    return col.every(f, opt_obj);
+  }
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    return goog.array.every(/** @type {!Array<?>} */ (col), f, opt_obj);
+  }
+  var keys = goog.structs.getKeys(col);
+  var values = goog.structs.getValues(col);
+  var l = values.length;
+  for (var i = 0; i < l; i++) {
+    if (!f.call(/** @type {?} */ (opt_obj), values[i], keys && keys[i], col)) {
+      return false;
+    }
+  }
+  return true;
+};
diff --git a/third_party/ink/closure/style/style.js b/third_party/ink/closure/style/style.js
new file mode 100644
index 0000000..ff44b099
--- /dev/null
+++ b/third_party/ink/closure/style/style.js
@@ -0,0 +1,2046 @@
+// Copyright 2006 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 Utilities for element styles.
+ *
+ * @author pupius@google.com (Daniel Pupius)
+ * @author arv@google.com (Erik Arvidsson)
+ * @author eae@google.com (Emil A Eklund)
+ * @author pallosp@google.com (Peter Pallos)
+ * @see ../demos/inline_block_quirks.html
+ * @see ../demos/inline_block_standards.html
+ * @see ../demos/style_viewport.html
+ */
+
+goog.provide('goog.style');
+
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.dom');
+goog.require('goog.dom.NodeType');
+goog.require('goog.dom.TagName');
+goog.require('goog.dom.vendor');
+goog.require('goog.html.SafeStyleSheet');
+goog.require('goog.math.Box');
+goog.require('goog.math.Coordinate');
+goog.require('goog.math.Rect');
+goog.require('goog.math.Size');
+goog.require('goog.object');
+goog.require('goog.reflect');
+goog.require('goog.string');
+goog.require('goog.userAgent');
+
+goog.forwardDeclare('goog.events.Event');
+
+
+/**
+ * Sets a style value on an element.
+ *
+ * This function is not indended to patch issues in the browser's style
+ * handling, but to allow easy programmatic access to setting dash-separated
+ * style properties.  An example is setting a batch of properties from a data
+ * object without overwriting old styles.  When possible, use native APIs:
+ * elem.style.propertyKey = 'value' or (if obliterating old styles is fine)
+ * elem.style.cssText = 'property1: value1; property2: value2'.
+ *
+ * @param {Element} element The element to change.
+ * @param {string|Object} style If a string, a style name. If an object, a hash
+ *     of style names to style values.
+ * @param {string|number|boolean=} opt_value If style was a string, then this
+ *     should be the value.
+ */
+goog.style.setStyle = function(element, style, opt_value) {
+  if (goog.isString(style)) {
+    goog.style.setStyle_(element, opt_value, style);
+  } else {
+    for (var key in style) {
+      goog.style.setStyle_(element, style[key], key);
+    }
+  }
+};
+
+
+/**
+ * Sets a style value on an element, with parameters swapped to work with
+ * {@code goog.object.forEach()}. Prepends a vendor-specific prefix when
+ * necessary.
+ * @param {Element} element The element to change.
+ * @param {string|number|boolean|undefined} value Style value.
+ * @param {string} style Style name.
+ * @private
+ */
+goog.style.setStyle_ = function(element, value, style) {
+  var propertyName = goog.style.getVendorJsStyleName_(element, style);
+
+  if (propertyName) {
+    // TODO(johnlenz): coerce to string?
+    element.style[propertyName] = /** @type {?} */ (value);
+  }
+};
+
+
+/**
+ * Style name cache that stores previous property name lookups.
+ *
+ * This is used by setStyle to speed up property lookups, entries look like:
+ *   { StyleName: ActualPropertyName }
+ *
+ * @private {!Object<string, string>}
+ */
+goog.style.styleNameCache_ = {};
+
+
+/**
+ * Returns the style property name in camel-case. If it does not exist and a
+ * vendor-specific version of the property does exist, then return the vendor-
+ * specific property name instead.
+ * @param {Element} element The element to change.
+ * @param {string} style Style name.
+ * @return {string} Vendor-specific style.
+ * @private
+ */
+goog.style.getVendorJsStyleName_ = function(element, style) {
+  var propertyName = goog.style.styleNameCache_[style];
+  if (!propertyName) {
+    var camelStyle = goog.string.toCamelCase(style);
+    propertyName = camelStyle;
+
+    if (element.style[camelStyle] === undefined) {
+      var prefixedStyle = goog.dom.vendor.getVendorJsPrefix() +
+          goog.string.toTitleCase(camelStyle);
+
+      if (element.style[prefixedStyle] !== undefined) {
+        propertyName = prefixedStyle;
+      }
+    }
+    goog.style.styleNameCache_[style] = propertyName;
+  }
+
+  return propertyName;
+};
+
+
+/**
+ * Returns the style property name in CSS notation. If it does not exist and a
+ * vendor-specific version of the property does exist, then return the vendor-
+ * specific property name instead.
+ * @param {Element} element The element to change.
+ * @param {string} style Style name.
+ * @return {string} Vendor-specific style.
+ * @private
+ */
+goog.style.getVendorStyleName_ = function(element, style) {
+  var camelStyle = goog.string.toCamelCase(style);
+
+  if (element.style[camelStyle] === undefined) {
+    var prefixedStyle = goog.dom.vendor.getVendorJsPrefix() +
+        goog.string.toTitleCase(camelStyle);
+
+    if (element.style[prefixedStyle] !== undefined) {
+      return goog.dom.vendor.getVendorPrefix() + '-' + style;
+    }
+  }
+
+  return style;
+};
+
+
+/**
+ * Retrieves an explicitly-set style value of a node. This returns '' if there
+ * isn't a style attribute on the element or if this style property has not been
+ * explicitly set in script.
+ *
+ * @param {Element} element Element to get style of.
+ * @param {string} property Property to get, css-style (if you have a camel-case
+ * property, use element.style[style]).
+ * @return {string} Style value.
+ */
+goog.style.getStyle = function(element, property) {
+  // element.style is '' for well-known properties which are unset.
+  // For for browser specific styles as 'filter' is undefined
+  // so we need to return '' explicitly to make it consistent across
+  // browsers.
+  var styleValue = element.style[goog.string.toCamelCase(property)];
+
+  // Using typeof here because of a bug in Safari 5.1, where this value
+  // was undefined, but === undefined returned false.
+  if (typeof(styleValue) !== 'undefined') {
+    return styleValue;
+  }
+
+  return element.style[goog.style.getVendorJsStyleName_(element, property)] ||
+      '';
+};
+
+
+/**
+ * Retrieves a computed style value of a node. It returns empty string if the
+ * value cannot be computed (which will be the case in Internet Explorer) or
+ * "none" if the property requested is an SVG one and it has not been
+ * explicitly set (firefox and webkit).
+ *
+ * @param {Element} element Element to get style of.
+ * @param {string} property Property to get (camel-case).
+ * @return {string} Style value.
+ */
+goog.style.getComputedStyle = function(element, property) {
+  var doc = goog.dom.getOwnerDocument(element);
+  if (doc.defaultView && doc.defaultView.getComputedStyle) {
+    var styles = doc.defaultView.getComputedStyle(element, null);
+    if (styles) {
+      // element.style[..] is undefined for browser specific styles
+      // as 'filter'.
+      return styles[property] || styles.getPropertyValue(property) || '';
+    }
+  }
+
+  return '';
+};
+
+
+/**
+ * Gets the cascaded style value of a node, or null if the value cannot be
+ * computed (only Internet Explorer can do this).
+ *
+ * @param {Element} element Element to get style of.
+ * @param {string} style Property to get (camel-case).
+ * @return {string} Style value.
+ */
+goog.style.getCascadedStyle = function(element, style) {
+  // TODO(nicksantos): This should be documented to return null. #fixTypes
+  return /** @type {string} */ (
+      element.currentStyle ? element.currentStyle[style] : null);
+};
+
+
+/**
+ * Cross-browser pseudo get computed style. It returns the computed style where
+ * available. If not available it tries the cascaded style value (IE
+ * currentStyle) and in worst case the inline style value.  It shouldn't be
+ * called directly, see http://wiki/Main/ComputedStyleVsCascadedStyle for
+ * discussion.
+ *
+ * @param {Element} element Element to get style of.
+ * @param {string} style Property to get (must be camelCase, not css-style.).
+ * @return {string} Style value.
+ * @private
+ */
+goog.style.getStyle_ = function(element, style) {
+  return goog.style.getComputedStyle(element, style) ||
+      goog.style.getCascadedStyle(element, style) ||
+      (element.style && element.style[style]);
+};
+
+
+/**
+ * Retrieves the computed value of the box-sizing CSS attribute.
+ * Browser support: http://caniuse.com/css3-boxsizing.
+ * @param {!Element} element The element whose box-sizing to get.
+ * @return {?string} 'content-box', 'border-box' or 'padding-box'. null if
+ *     box-sizing is not supported (IE7 and below).
+ */
+goog.style.getComputedBoxSizing = function(element) {
+  return goog.style.getStyle_(element, 'boxSizing') ||
+      goog.style.getStyle_(element, 'MozBoxSizing') ||
+      goog.style.getStyle_(element, 'WebkitBoxSizing') || null;
+};
+
+
+/**
+ * Retrieves the computed value of the position CSS attribute.
+ * @param {Element} element The element to get the position of.
+ * @return {string} Position value.
+ */
+goog.style.getComputedPosition = function(element) {
+  return goog.style.getStyle_(element, 'position');
+};
+
+
+/**
+ * Retrieves the computed background color string for a given element. The
+ * string returned is suitable for assigning to another element's
+ * background-color, but is not guaranteed to be in any particular string
+ * format. Accessing the color in a numeric form may not be possible in all
+ * browsers or with all input.
+ *
+ * If the background color for the element is defined as a hexadecimal value,
+ * the resulting string can be parsed by goog.color.parse in all supported
+ * browsers.
+ *
+ * Whether named colors like "red" or "lightblue" get translated into a
+ * format which can be parsed is browser dependent. Calling this function on
+ * transparent elements will return "transparent" in most browsers or
+ * "rgba(0, 0, 0, 0)" in WebKit.
+ * @param {Element} element The element to get the background color of.
+ * @return {string} The computed string value of the background color.
+ */
+goog.style.getBackgroundColor = function(element) {
+  return goog.style.getStyle_(element, 'backgroundColor');
+};
+
+
+/**
+ * Retrieves the computed value of the overflow-x CSS attribute.
+ * @param {Element} element The element to get the overflow-x of.
+ * @return {string} The computed string value of the overflow-x attribute.
+ */
+goog.style.getComputedOverflowX = function(element) {
+  return goog.style.getStyle_(element, 'overflowX');
+};
+
+
+/**
+ * Retrieves the computed value of the overflow-y CSS attribute.
+ * @param {Element} element The element to get the overflow-y of.
+ * @return {string} The computed string value of the overflow-y attribute.
+ */
+goog.style.getComputedOverflowY = function(element) {
+  return goog.style.getStyle_(element, 'overflowY');
+};
+
+
+/**
+ * Retrieves the computed value of the z-index CSS attribute.
+ * @param {Element} element The element to get the z-index of.
+ * @return {string|number} The computed value of the z-index attribute.
+ */
+goog.style.getComputedZIndex = function(element) {
+  return goog.style.getStyle_(element, 'zIndex');
+};
+
+
+/**
+ * Retrieves the computed value of the text-align CSS attribute.
+ * @param {Element} element The element to get the text-align of.
+ * @return {string} The computed string value of the text-align attribute.
+ */
+goog.style.getComputedTextAlign = function(element) {
+  return goog.style.getStyle_(element, 'textAlign');
+};
+
+
+/**
+ * Retrieves the computed value of the cursor CSS attribute.
+ * @param {Element} element The element to get the cursor of.
+ * @return {string} The computed string value of the cursor attribute.
+ */
+goog.style.getComputedCursor = function(element) {
+  return goog.style.getStyle_(element, 'cursor');
+};
+
+
+/**
+ * Retrieves the computed value of the CSS transform attribute.
+ * @param {Element} element The element to get the transform of.
+ * @return {string} The computed string representation of the transform matrix.
+ */
+goog.style.getComputedTransform = function(element) {
+  var property = goog.style.getVendorStyleName_(element, 'transform');
+  return goog.style.getStyle_(element, property) ||
+      goog.style.getStyle_(element, 'transform');
+};
+
+
+/**
+ * Sets the top/left values of an element.  If no unit is specified in the
+ * argument then it will add px. The second argument is required if the first
+ * argument is a string or number and is ignored if the first argument
+ * is a coordinate.
+ * @param {Element} el Element to move.
+ * @param {string|number|goog.math.Coordinate} arg1 Left position or coordinate.
+ * @param {string|number=} opt_arg2 Top position.
+ */
+goog.style.setPosition = function(el, arg1, opt_arg2) {
+  var x, y;
+
+  if (arg1 instanceof goog.math.Coordinate) {
+    x = arg1.x;
+    y = arg1.y;
+  } else {
+    x = arg1;
+    y = opt_arg2;
+  }
+
+  el.style.left = goog.style.getPixelStyleValue_(
+      /** @type {number|string} */ (x), false);
+  el.style.top = goog.style.getPixelStyleValue_(
+      /** @type {number|string} */ (y), false);
+};
+
+
+/**
+ * Gets the offsetLeft and offsetTop properties of an element and returns them
+ * in a Coordinate object
+ * @param {Element} element Element.
+ * @return {!goog.math.Coordinate} The position.
+ */
+goog.style.getPosition = function(element) {
+  return new goog.math.Coordinate(
+      /** @type {!HTMLElement} */ (element).offsetLeft,
+      /** @type {!HTMLElement} */ (element).offsetTop);
+};
+
+
+/**
+ * Returns the viewport element for a particular document
+ * @param {Node=} opt_node DOM node (Document is OK) to get the viewport element
+ *     of.
+ * @return {Element} document.documentElement or document.body.
+ */
+goog.style.getClientViewportElement = function(opt_node) {
+  var doc;
+  if (opt_node) {
+    doc = goog.dom.getOwnerDocument(opt_node);
+  } else {
+    doc = goog.dom.getDocument();
+  }
+
+  // In old IE versions the document.body represented the viewport
+  if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9) &&
+      !goog.dom.getDomHelper(doc).isCss1CompatMode()) {
+    return doc.body;
+  }
+  return doc.documentElement;
+};
+
+
+/**
+ * Calculates the viewport coordinates relative to the page/document
+ * containing the node. The viewport may be the browser viewport for
+ * non-iframe document, or the iframe container for iframe'd document.
+ * @param {!Document} doc The document to use as the reference point.
+ * @return {!goog.math.Coordinate} The page offset of the viewport.
+ */
+goog.style.getViewportPageOffset = function(doc) {
+  var body = doc.body;
+  var documentElement = doc.documentElement;
+  var scrollLeft = body.scrollLeft || documentElement.scrollLeft;
+  var scrollTop = body.scrollTop || documentElement.scrollTop;
+  return new goog.math.Coordinate(scrollLeft, scrollTop);
+};
+
+
+/**
+ * Gets the client rectangle of the DOM element.
+ *
+ * getBoundingClientRect is part of a new CSS object model draft (with a
+ * long-time presence in IE), replacing the error-prone parent offset
+ * computation and the now-deprecated Gecko getBoxObjectFor.
+ *
+ * This utility patches common browser bugs in getBoundingClientRect. It
+ * will fail if getBoundingClientRect is unsupported.
+ *
+ * If the element is not in the DOM, the result is undefined, and an error may
+ * be thrown depending on user agent.
+ *
+ * @param {!Element} el The element whose bounding rectangle is being queried.
+ * @return {Object} A native bounding rectangle with numerical left, top,
+ *     right, and bottom.  Reported by Firefox to be of object type ClientRect.
+ * @private
+ */
+goog.style.getBoundingClientRect_ = function(el) {
+  var rect;
+  try {
+    rect = el.getBoundingClientRect();
+  } catch (e) {
+    // In IE < 9, calling getBoundingClientRect on an orphan element raises an
+    // "Unspecified Error". All other browsers return zeros.
+    return {'left': 0, 'top': 0, 'right': 0, 'bottom': 0};
+  }
+
+  // Patch the result in IE only, so that this function can be inlined if
+  // compiled for non-IE.
+  if (goog.userAgent.IE && el.ownerDocument.body) {
+    // In IE, most of the time, 2 extra pixels are added to the top and left
+    // due to the implicit 2-pixel inset border.  In IE6/7 quirks mode and
+    // IE6 standards mode, this border can be overridden by setting the
+    // document element's border to zero -- thus, we cannot rely on the
+    // offset always being 2 pixels.
+
+    // In quirks mode, the offset can be determined by querying the body's
+    // clientLeft/clientTop, but in standards mode, it is found by querying
+    // the document element's clientLeft/clientTop.  Since we already called
+    // getBoundingClientRect we have already forced a reflow, so it is not
+    // too expensive just to query them all.
+
+    // See: http://msdn.microsoft.com/en-us/library/ms536433(VS.85).aspx
+    var doc = el.ownerDocument;
+    rect.left -= doc.documentElement.clientLeft + doc.body.clientLeft;
+    rect.top -= doc.documentElement.clientTop + doc.body.clientTop;
+  }
+  return rect;
+};
+
+
+/**
+ * Returns the first parent that could affect the position of a given element.
+ * @param {Element} element The element to get the offset parent for.
+ * @return {Element} The first offset parent or null if one cannot be found.
+ */
+goog.style.getOffsetParent = function(element) {
+  // element.offsetParent does the right thing in IE7 and below.  In other
+  // browsers it only includes elements with position absolute, relative or
+  // fixed, not elements with overflow set to auto or scroll.
+  if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(8)) {
+    goog.asserts.assert(element && 'offsetParent' in element);
+    return element.offsetParent;
+  }
+
+  var doc = goog.dom.getOwnerDocument(element);
+  var positionStyle = goog.style.getStyle_(element, 'position');
+  var skipStatic = positionStyle == 'fixed' || positionStyle == 'absolute';
+  for (var parent = element.parentNode; parent && parent != doc;
+       parent = parent.parentNode) {
+    // Skip shadowDOM roots.
+    if (parent.nodeType == goog.dom.NodeType.DOCUMENT_FRAGMENT && parent.host) {
+      parent = parent.host;
+    }
+    positionStyle =
+        goog.style.getStyle_(/** @type {!Element} */ (parent), 'position');
+    skipStatic = skipStatic && positionStyle == 'static' &&
+        parent != doc.documentElement && parent != doc.body;
+    if (!skipStatic &&
+        (parent.scrollWidth > parent.clientWidth ||
+         parent.scrollHeight > parent.clientHeight ||
+         positionStyle == 'fixed' || positionStyle == 'absolute' ||
+         positionStyle == 'relative')) {
+      return /** @type {!Element} */ (parent);
+    }
+  }
+  return null;
+};
+
+
+/**
+ * Calculates and returns the visible rectangle for a given element. Returns a
+ * box describing the visible portion of the nearest scrollable offset ancestor.
+ * Coordinates are given relative to the document.
+ *
+ * @param {Element} element Element to get the visible rect for.
+ * @return {goog.math.Box} Bounding elementBox describing the visible rect or
+ *     null if scrollable ancestor isn't inside the visible viewport.
+ */
+goog.style.getVisibleRectForElement = function(element) {
+  var visibleRect = new goog.math.Box(0, Infinity, Infinity, 0);
+  var dom = goog.dom.getDomHelper(element);
+  var body = dom.getDocument().body;
+  var documentElement = dom.getDocument().documentElement;
+  var scrollEl = dom.getDocumentScrollElement();
+
+  // Determine the size of the visible rect by climbing the dom accounting for
+  // all scrollable containers.
+  for (var el = element; el = goog.style.getOffsetParent(el);) {
+    // clientWidth is zero for inline block elements in IE.
+    // on WEBKIT, body element can have clientHeight = 0 and scrollHeight > 0
+    if ((!goog.userAgent.IE || el.clientWidth != 0) &&
+        (!goog.userAgent.WEBKIT || el.clientHeight != 0 || el != body) &&
+        // body may have overflow set on it, yet we still get the entire
+        // viewport. In some browsers, el.offsetParent may be
+        // document.documentElement, so check for that too.
+        (el != body && el != documentElement &&
+         goog.style.getStyle_(el, 'overflow') != 'visible')) {
+      var pos = goog.style.getPageOffset(el);
+      var client = goog.style.getClientLeftTop(el);
+      pos.x += client.x;
+      pos.y += client.y;
+
+      visibleRect.top = Math.max(visibleRect.top, pos.y);
+      visibleRect.right = Math.min(visibleRect.right, pos.x + el.clientWidth);
+      visibleRect.bottom =
+          Math.min(visibleRect.bottom, pos.y + el.clientHeight);
+      visibleRect.left = Math.max(visibleRect.left, pos.x);
+    }
+  }
+
+  // Clip by window's viewport.
+  var scrollX = scrollEl.scrollLeft, scrollY = scrollEl.scrollTop;
+  visibleRect.left = Math.max(visibleRect.left, scrollX);
+  visibleRect.top = Math.max(visibleRect.top, scrollY);
+  var winSize = dom.getViewportSize();
+  visibleRect.right = Math.min(visibleRect.right, scrollX + winSize.width);
+  visibleRect.bottom = Math.min(visibleRect.bottom, scrollY + winSize.height);
+  return visibleRect.top >= 0 && visibleRect.left >= 0 &&
+          visibleRect.bottom > visibleRect.top &&
+          visibleRect.right > visibleRect.left ?
+      visibleRect :
+      null;
+};
+
+
+/**
+ * Calculate the scroll position of {@code container} with the minimum amount so
+ * that the content and the borders of the given {@code element} become visible.
+ * If the element is bigger than the container, its top left corner will be
+ * aligned as close to the container's top left corner as possible.
+ *
+ * @param {Element} element The element to make visible.
+ * @param {Element=} opt_container The container to scroll. If not set, then the
+ *     document scroll element will be used.
+ * @param {boolean=} opt_center Whether to center the element in the container.
+ *     Defaults to false.
+ * @return {!goog.math.Coordinate} The new scroll position of the container,
+ *     in form of goog.math.Coordinate(scrollLeft, scrollTop).
+ */
+goog.style.getContainerOffsetToScrollInto = function(
+    element, opt_container, opt_center) {
+  var container = opt_container || goog.dom.getDocumentScrollElement();
+  // Absolute position of the element's border's top left corner.
+  var elementPos = goog.style.getPageOffset(element);
+  // Absolute position of the container's border's top left corner.
+  var containerPos = goog.style.getPageOffset(container);
+  var containerBorder = goog.style.getBorderBox(container);
+  if (container == goog.dom.getDocumentScrollElement()) {
+    // The element position is calculated based on the page offset, and the
+    // document scroll element holds the scroll position within the page. We can
+    // use the scroll position to calculate the relative position from the
+    // element.
+    var relX = elementPos.x - container.scrollLeft;
+    var relY = elementPos.y - container.scrollTop;
+    if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(10)) {
+      // In older versions of IE getPageOffset(element) does not include the
+      // container border so it has to be added to accommodate.
+      relX += containerBorder.left;
+      relY += containerBorder.top;
+    }
+  } else {
+    // Relative pos. of the element's border box to the container's content box.
+    var relX = elementPos.x - containerPos.x - containerBorder.left;
+    var relY = elementPos.y - containerPos.y - containerBorder.top;
+  }
+  // How much the element can move in the container, i.e. the difference between
+  // the element's bottom-right-most and top-left-most position where it's
+  // fully visible.
+  var elementSize = goog.style.getSizeWithDisplay_(element);
+  var spaceX = container.clientWidth - elementSize.width;
+  var spaceY = container.clientHeight - elementSize.height;
+  var scrollLeft = container.scrollLeft;
+  var scrollTop = container.scrollTop;
+  if (opt_center) {
+    // All browsers round non-integer scroll positions down.
+    scrollLeft += relX - spaceX / 2;
+    scrollTop += relY - spaceY / 2;
+  } else {
+    // This formula was designed to give the correct scroll values in the
+    // following cases:
+    // - element is higher than container (spaceY < 0) => scroll down by relY
+    // - element is not higher that container (spaceY >= 0):
+    //   - it is above container (relY < 0) => scroll up by abs(relY)
+    //   - it is below container (relY > spaceY) => scroll down by relY - spaceY
+    //   - it is in the container => don't scroll
+    scrollLeft += Math.min(relX, Math.max(relX - spaceX, 0));
+    scrollTop += Math.min(relY, Math.max(relY - spaceY, 0));
+  }
+  return new goog.math.Coordinate(scrollLeft, scrollTop);
+};
+
+
+/**
+ * Changes the scroll position of {@code container} with the minimum amount so
+ * that the content and the borders of the given {@code element} become visible.
+ * If the element is bigger than the container, its top left corner will be
+ * aligned as close to the container's top left corner as possible.
+ *
+ * @param {Element} element The element to make visible.
+ * @param {Element=} opt_container The container to scroll. If not set, then the
+ *     document scroll element will be used.
+ * @param {boolean=} opt_center Whether to center the element in the container.
+ *     Defaults to false.
+ */
+goog.style.scrollIntoContainerView = function(
+    element, opt_container, opt_center) {
+  var container = opt_container || goog.dom.getDocumentScrollElement();
+  var offset =
+      goog.style.getContainerOffsetToScrollInto(element, container, opt_center);
+  container.scrollLeft = offset.x;
+  container.scrollTop = offset.y;
+};
+
+
+/**
+ * Returns clientLeft (width of the left border and, if the directionality is
+ * right to left, the vertical scrollbar) and clientTop as a coordinate object.
+ *
+ * @param {Element} el Element to get clientLeft for.
+ * @return {!goog.math.Coordinate} Client left and top.
+ */
+goog.style.getClientLeftTop = function(el) {
+  return new goog.math.Coordinate(el.clientLeft, el.clientTop);
+};
+
+
+/**
+ * Returns a Coordinate object relative to the top-left of the HTML document.
+ * Implemented as a single function to save having to do two recursive loops in
+ * opera and safari just to get both coordinates.  If you just want one value do
+ * use goog.style.getPageOffsetLeft() and goog.style.getPageOffsetTop(), but
+ * note if you call both those methods the tree will be analysed twice.
+ *
+ * @param {Element} el Element to get the page offset for.
+ * @return {!goog.math.Coordinate} The page offset.
+ */
+goog.style.getPageOffset = function(el) {
+  var doc = goog.dom.getOwnerDocument(el);
+  // TODO(gboyer): Update the jsdoc in a way that doesn't break the universe.
+  goog.asserts.assertObject(el, 'Parameter is required');
+
+  // NOTE(arv): If element is hidden (display none or disconnected or any the
+  // ancestors are hidden) we get (0,0) by default but we still do the
+  // accumulation of scroll position.
+
+  // TODO(arv): Should we check if the node is disconnected and in that case
+  //            return (0,0)?
+
+  var pos = new goog.math.Coordinate(0, 0);
+  var viewportElement = goog.style.getClientViewportElement(doc);
+  if (el == viewportElement) {
+    // viewport is always at 0,0 as that defined the coordinate system for this
+    // function - this avoids special case checks in the code below
+    return pos;
+  }
+
+  var box = goog.style.getBoundingClientRect_(el);
+  // Must add the scroll coordinates in to get the absolute page offset
+  // of element since getBoundingClientRect returns relative coordinates to
+  // the viewport.
+  var scrollCoord = goog.dom.getDomHelper(doc).getDocumentScroll();
+  pos.x = box.left + scrollCoord.x;
+  pos.y = box.top + scrollCoord.y;
+
+  return pos;
+};
+
+
+/**
+ * Returns the left coordinate of an element relative to the HTML document
+ * @param {Element} el Elements.
+ * @return {number} The left coordinate.
+ */
+goog.style.getPageOffsetLeft = function(el) {
+  return goog.style.getPageOffset(el).x;
+};
+
+
+/**
+ * Returns the top coordinate of an element relative to the HTML document
+ * @param {Element} el Elements.
+ * @return {number} The top coordinate.
+ */
+goog.style.getPageOffsetTop = function(el) {
+  return goog.style.getPageOffset(el).y;
+};
+
+
+/**
+ * Returns a Coordinate object relative to the top-left of an HTML document
+ * in an ancestor frame of this element. Used for measuring the position of
+ * an element inside a frame relative to a containing frame.
+ *
+ * @param {Element} el Element to get the page offset for.
+ * @param {Window} relativeWin The window to measure relative to. If relativeWin
+ *     is not in the ancestor frame chain of the element, we measure relative to
+ *     the top-most window.
+ * @return {!goog.math.Coordinate} The page offset.
+ */
+goog.style.getFramedPageOffset = function(el, relativeWin) {
+  var position = new goog.math.Coordinate(0, 0);
+
+  // Iterate up the ancestor frame chain, keeping track of the current window
+  // and the current element in that window.
+  var currentWin = goog.dom.getWindow(goog.dom.getOwnerDocument(el));
+
+  // MS Edge throws when accessing "parent" if el's containing iframe has been
+  // deleted.
+  if (!goog.reflect.canAccessProperty(currentWin, 'parent')) {
+    return position;
+  }
+
+  var currentEl = el;
+  do {
+    // if we're at the top window, we want to get the page offset.
+    // if we're at an inner frame, we only want to get the window position
+    // so that we can determine the actual page offset in the context of
+    // the outer window.
+    var offset = currentWin == relativeWin ?
+        goog.style.getPageOffset(currentEl) :
+        goog.style.getClientPositionForElement_(goog.asserts.assert(currentEl));
+
+    position.x += offset.x;
+    position.y += offset.y;
+  } while (currentWin && currentWin != relativeWin &&
+           currentWin != currentWin.parent &&
+           (currentEl = currentWin.frameElement) &&
+           (currentWin = currentWin.parent));
+
+  return position;
+};
+
+
+/**
+ * Translates the specified rect relative to origBase page, for newBase page.
+ * If origBase and newBase are the same, this function does nothing.
+ *
+ * @param {goog.math.Rect} rect The source rectangle relative to origBase page,
+ *     and it will have the translated result.
+ * @param {goog.dom.DomHelper} origBase The DomHelper for the input rectangle.
+ * @param {goog.dom.DomHelper} newBase The DomHelper for the resultant
+ *     coordinate.  This must be a DOM for an ancestor frame of origBase
+ *     or the same as origBase.
+ */
+goog.style.translateRectForAnotherFrame = function(rect, origBase, newBase) {
+  if (origBase.getDocument() != newBase.getDocument()) {
+    var body = origBase.getDocument().body;
+    var pos = goog.style.getFramedPageOffset(body, newBase.getWindow());
+
+    // Adjust Body's margin.
+    pos = goog.math.Coordinate.difference(pos, goog.style.getPageOffset(body));
+
+    if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9) &&
+        !origBase.isCss1CompatMode()) {
+      pos = goog.math.Coordinate.difference(pos, origBase.getDocumentScroll());
+    }
+
+    rect.left += pos.x;
+    rect.top += pos.y;
+  }
+};
+
+
+/**
+ * Returns the position of an element relative to another element in the
+ * document.  A relative to B
+ * @param {Element|Event|goog.events.Event} a Element or mouse event whose
+ *     position we're calculating.
+ * @param {Element|Event|goog.events.Event} b Element or mouse event position
+ *     is relative to.
+ * @return {!goog.math.Coordinate} The relative position.
+ */
+goog.style.getRelativePosition = function(a, b) {
+  var ap = goog.style.getClientPosition(a);
+  var bp = goog.style.getClientPosition(b);
+  return new goog.math.Coordinate(ap.x - bp.x, ap.y - bp.y);
+};
+
+
+/**
+ * Returns the position of the event or the element's border box relative to
+ * the client viewport.
+ * @param {!Element} el Element whose position to get.
+ * @return {!goog.math.Coordinate} The position.
+ * @private
+ */
+goog.style.getClientPositionForElement_ = function(el) {
+  var box = goog.style.getBoundingClientRect_(el);
+  return new goog.math.Coordinate(box.left, box.top);
+};
+
+
+/**
+ * Returns the position of the event or the element's border box relative to
+ * the client viewport. If an event is passed, and if this event is a "touch"
+ * event, then the position of the first changedTouches will be returned.
+ * @param {Element|Event|goog.events.Event} el Element or a mouse / touch event.
+ * @return {!goog.math.Coordinate} The position.
+ */
+goog.style.getClientPosition = function(el) {
+  goog.asserts.assert(el);
+  if (el.nodeType == goog.dom.NodeType.ELEMENT) {
+    return goog.style.getClientPositionForElement_(
+        /** @type {!Element} */ (el));
+  } else {
+    var targetEvent = el.changedTouches ? el.changedTouches[0] : el;
+    return new goog.math.Coordinate(targetEvent.clientX, targetEvent.clientY);
+  }
+};
+
+
+/**
+ * Moves an element to the given coordinates relative to the client viewport.
+ * @param {Element} el Absolutely positioned element to set page offset for.
+ *     It must be in the document.
+ * @param {number|goog.math.Coordinate} x Left position of the element's margin
+ *     box or a coordinate object.
+ * @param {number=} opt_y Top position of the element's margin box.
+ */
+goog.style.setPageOffset = function(el, x, opt_y) {
+  // Get current pageoffset
+  var cur = goog.style.getPageOffset(el);
+
+  if (x instanceof goog.math.Coordinate) {
+    opt_y = x.y;
+    x = x.x;
+  }
+
+  // NOTE(arv): We cannot allow strings for x and y. We could but that would
+  // require us to manually transform between different units
+
+  // Work out deltas
+  var dx = goog.asserts.assertNumber(x) - cur.x;
+  var dy = Number(opt_y) - cur.y;
+
+  // Set position to current left/top + delta
+  goog.style.setPosition(
+      el, /** @type {!HTMLElement} */ (el).offsetLeft + dx,
+      /** @type {!HTMLElement} */ (el).offsetTop + dy);
+};
+
+
+/**
+ * Sets the width/height values of an element.  If an argument is numeric,
+ * or a goog.math.Size is passed, it is assumed to be pixels and will add
+ * 'px' after converting it to an integer in string form. (This just sets the
+ * CSS width and height properties so it might set content-box or border-box
+ * size depending on the box model the browser is using.)
+ *
+ * @param {Element} element Element to set the size of.
+ * @param {string|number|goog.math.Size} w Width of the element, or a
+ *     size object.
+ * @param {string|number=} opt_h Height of the element. Required if w is not a
+ *     size object.
+ */
+goog.style.setSize = function(element, w, opt_h) {
+  var h;
+  if (w instanceof goog.math.Size) {
+    h = w.height;
+    w = w.width;
+  } else {
+    if (opt_h == undefined) {
+      throw new Error('missing height argument');
+    }
+    h = opt_h;
+  }
+
+  goog.style.setWidth(element, /** @type {string|number} */ (w));
+  goog.style.setHeight(element, h);
+};
+
+
+/**
+ * Helper function to create a string to be set into a pixel-value style
+ * property of an element. Can round to the nearest integer value.
+ *
+ * @param {string|number} value The style value to be used. If a number,
+ *     'px' will be appended, otherwise the value will be applied directly.
+ * @param {boolean} round Whether to round the nearest integer (if property
+ *     is a number).
+ * @return {string} The string value for the property.
+ * @private
+ */
+goog.style.getPixelStyleValue_ = function(value, round) {
+  if (typeof value == 'number') {
+    value = (round ? Math.round(value) : value) + 'px';
+  }
+
+  return value;
+};
+
+
+/**
+ * Set the height of an element.  Sets the element's style property.
+ * @param {Element} element Element to set the height of.
+ * @param {string|number} height The height value to set.  If a number, 'px'
+ *     will be appended, otherwise the value will be applied directly.
+ */
+goog.style.setHeight = function(element, height) {
+  element.style.height = goog.style.getPixelStyleValue_(height, true);
+};
+
+
+/**
+ * Set the width of an element.  Sets the element's style property.
+ * @param {Element} element Element to set the width of.
+ * @param {string|number} width The width value to set.  If a number, 'px'
+ *     will be appended, otherwise the value will be applied directly.
+ */
+goog.style.setWidth = function(element, width) {
+  element.style.width = goog.style.getPixelStyleValue_(width, true);
+};
+
+
+/**
+ * Gets the height and width of an element, even if its display is none.
+ *
+ * Specifically, this returns the height and width of the border box,
+ * irrespective of the box model in effect.
+ *
+ * Note that this function does not take CSS transforms into account. Please see
+ * {@code goog.style.getTransformedSize}.
+ * @param {Element} element Element to get size of.
+ * @return {!goog.math.Size} Object with width/height properties.
+ */
+goog.style.getSize = function(element) {
+  return goog.style.evaluateWithTemporaryDisplay_(
+      goog.style.getSizeWithDisplay_, /** @type {!Element} */ (element));
+};
+
+
+/**
+ * Call {@code fn} on {@code element} such that {@code element}'s dimensions are
+ * accurate when it's passed to {@code fn}.
+ * @param {function(!Element): T} fn Function to call with {@code element} as
+ *     an argument after temporarily changing {@code element}'s display such
+ *     that its dimensions are accurate.
+ * @param {!Element} element Element (which may have display none) to use as
+ *     argument to {@code fn}.
+ * @return {T} Value returned by calling {@code fn} with {@code element}.
+ * @template T
+ * @private
+ */
+goog.style.evaluateWithTemporaryDisplay_ = function(fn, element) {
+  if (goog.style.getStyle_(element, 'display') != 'none') {
+    return fn(element);
+  }
+
+  var style = element.style;
+  var originalDisplay = style.display;
+  var originalVisibility = style.visibility;
+  var originalPosition = style.position;
+
+  style.visibility = 'hidden';
+  style.position = 'absolute';
+  style.display = 'inline';
+
+  var retVal = fn(element);
+
+  style.display = originalDisplay;
+  style.position = originalPosition;
+  style.visibility = originalVisibility;
+
+  return retVal;
+};
+
+
+/**
+ * Gets the height and width of an element when the display is not none.
+ * @param {Element} element Element to get size of.
+ * @return {!goog.math.Size} Object with width/height properties.
+ * @private
+ */
+goog.style.getSizeWithDisplay_ = function(element) {
+  var offsetWidth = /** @type {!HTMLElement} */ (element).offsetWidth;
+  var offsetHeight = /** @type {!HTMLElement} */ (element).offsetHeight;
+  var webkitOffsetsZero =
+      goog.userAgent.WEBKIT && !offsetWidth && !offsetHeight;
+  if ((!goog.isDef(offsetWidth) || webkitOffsetsZero) &&
+      element.getBoundingClientRect) {
+    // Fall back to calling getBoundingClientRect when offsetWidth or
+    // offsetHeight are not defined, or when they are zero in WebKit browsers.
+    // This makes sure that we return for the correct size for SVG elements, but
+    // will still return 0 on Webkit prior to 534.8, see
+    // http://trac.webkit.org/changeset/67252.
+    var clientRect = goog.style.getBoundingClientRect_(element);
+    return new goog.math.Size(
+        clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
+  }
+  return new goog.math.Size(offsetWidth, offsetHeight);
+};
+
+
+/**
+ * Gets the height and width of an element, post transform, even if its display
+ * is none.
+ *
+ * This is like {@code goog.style.getSize}, except:
+ * <ol>
+ * <li>Takes webkitTransforms such as rotate and scale into account.
+ * <li>Will return null if {@code element} doesn't respond to
+ *     {@code getBoundingClientRect}.
+ * <li>Currently doesn't make sense on non-WebKit browsers which don't support
+ *    webkitTransforms.
+ * </ol>
+ * @param {!Element} element Element to get size of.
+ * @return {goog.math.Size} Object with width/height properties.
+ */
+goog.style.getTransformedSize = function(element) {
+  if (!element.getBoundingClientRect) {
+    return null;
+  }
+
+  var clientRect = goog.style.evaluateWithTemporaryDisplay_(
+      goog.style.getBoundingClientRect_, element);
+  return new goog.math.Size(
+      clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
+};
+
+
+/**
+ * Returns a bounding rectangle for a given element in page space.
+ * @param {Element} element Element to get bounds of. Must not be display none.
+ * @return {!goog.math.Rect} Bounding rectangle for the element.
+ */
+goog.style.getBounds = function(element) {
+  var o = goog.style.getPageOffset(element);
+  var s = goog.style.getSize(element);
+  return new goog.math.Rect(o.x, o.y, s.width, s.height);
+};
+
+
+/**
+ * Converts a CSS selector in the form style-property to styleProperty.
+ * @param {*} selector CSS Selector.
+ * @return {string} Camel case selector.
+ * @deprecated Use goog.string.toCamelCase instead.
+ */
+goog.style.toCamelCase = function(selector) {
+  return goog.string.toCamelCase(String(selector));
+};
+
+
+/**
+ * Converts a CSS selector in the form styleProperty to style-property.
+ * @param {string} selector Camel case selector.
+ * @return {string} Selector cased.
+ * @deprecated Use goog.string.toSelectorCase instead.
+ */
+goog.style.toSelectorCase = function(selector) {
+  return goog.string.toSelectorCase(selector);
+};
+
+
+/**
+ * Gets the opacity of a node (x-browser). This gets the inline style opacity
+ * of the node, and does not take into account the cascaded or the computed
+ * style for this node.
+ * @param {Element} el Element whose opacity has to be found.
+ * @return {number|string} Opacity between 0 and 1 or an empty string {@code ''}
+ *     if the opacity is not set.
+ */
+goog.style.getOpacity = function(el) {
+  goog.asserts.assert(el);
+  var style = el.style;
+  var result = '';
+  if ('opacity' in style) {
+    result = style.opacity;
+  } else if ('MozOpacity' in style) {
+    result = style.MozOpacity;
+  } else if ('filter' in style) {
+    var match = style.filter.match(/alpha\(opacity=([\d.]+)\)/);
+    if (match) {
+      result = String(match[1] / 100);
+    }
+  }
+  return result == '' ? result : Number(result);
+};
+
+
+/**
+ * Sets the opacity of a node (x-browser).
+ * @param {Element} el Elements whose opacity has to be set.
+ * @param {number|string} alpha Opacity between 0 and 1 or an empty string
+ *     {@code ''} to clear the opacity.
+ */
+goog.style.setOpacity = function(el, alpha) {
+  goog.asserts.assert(el);
+  var style = el.style;
+  if ('opacity' in style) {
+    style.opacity = alpha;
+  } else if ('MozOpacity' in style) {
+    style.MozOpacity = alpha;
+  } else if ('filter' in style) {
+    // TODO(arv): Overwriting the filter might have undesired side effects.
+    if (alpha === '') {
+      style.filter = '';
+    } else {
+      style.filter = 'alpha(opacity=' + (Number(alpha) * 100) + ')';
+    }
+  }
+};
+
+
+/**
+ * Sets the background of an element to a transparent image in a browser-
+ * independent manner.
+ *
+ * This function does not support repeating backgrounds or alternate background
+ * positions to match the behavior of Internet Explorer. It also does not
+ * support sizingMethods other than crop since they cannot be replicated in
+ * browsers other than Internet Explorer.
+ *
+ * @param {Element} el The element to set background on.
+ * @param {string} src The image source URL.
+ */
+goog.style.setTransparentBackgroundImage = function(el, src) {
+  var style = el.style;
+  // It is safe to use the style.filter in IE only. In Safari 'filter' is in
+  // style object but access to style.filter causes it to throw an exception.
+  // Note: IE8 supports images with an alpha channel.
+  if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('8')) {
+    // See TODO in setOpacity.
+    style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(' +
+        'src="' + src + '", sizingMethod="crop")';
+  } else {
+    // Set style properties individually instead of using background shorthand
+    // to prevent overwriting a pre-existing background color.
+    style.backgroundImage = 'url(' + src + ')';
+    style.backgroundPosition = 'top left';
+    style.backgroundRepeat = 'no-repeat';
+  }
+};
+
+
+/**
+ * Clears the background image of an element in a browser independent manner.
+ * @param {Element} el The element to clear background image for.
+ */
+goog.style.clearTransparentBackgroundImage = function(el) {
+  var style = el.style;
+  if ('filter' in style) {
+    // See TODO in setOpacity.
+    style.filter = '';
+  } else {
+    // Set style properties individually instead of using background shorthand
+    // to prevent overwriting a pre-existing background color.
+    style.backgroundImage = 'none';
+  }
+};
+
+
+/**
+ * Shows or hides an element from the page. Hiding the element is done by
+ * setting the display property to "none", removing the element from the
+ * rendering hierarchy so it takes up no space. To show the element, the default
+ * inherited display property is restored (defined either in stylesheets or by
+ * the browser's default style rules.)
+ *
+ * Caveat 1: if the inherited display property for the element is set to "none"
+ * by the stylesheets, that is the property that will be restored by a call to
+ * showElement(), effectively toggling the display between "none" and "none".
+ *
+ * Caveat 2: if the element display style is set inline (by setting either
+ * element.style.display or a style attribute in the HTML), a call to
+ * showElement will clear that setting and defer to the inherited style in the
+ * stylesheet.
+ * @param {Element} el Element to show or hide.
+ * @param {*} display True to render the element in its default style,
+ *     false to disable rendering the element.
+ * @deprecated Use goog.style.setElementShown instead.
+ */
+goog.style.showElement = function(el, display) {
+  goog.style.setElementShown(el, display);
+};
+
+
+/**
+ * Shows or hides an element from the page. Hiding the element is done by
+ * setting the display property to "none", removing the element from the
+ * rendering hierarchy so it takes up no space. To show the element, the default
+ * inherited display property is restored (defined either in stylesheets or by
+ * the browser's default style rules).
+ *
+ * Caveat 1: if the inherited display property for the element is set to "none"
+ * by the stylesheets, that is the property that will be restored by a call to
+ * setElementShown(), effectively toggling the display between "none" and
+ * "none".
+ *
+ * Caveat 2: if the element display style is set inline (by setting either
+ * element.style.display or a style attribute in the HTML), a call to
+ * setElementShown will clear that setting and defer to the inherited style in
+ * the stylesheet.
+ * @param {Element} el Element to show or hide.
+ * @param {*} isShown True to render the element in its default style,
+ *     false to disable rendering the element.
+ */
+goog.style.setElementShown = function(el, isShown) {
+  el.style.display = isShown ? '' : 'none';
+};
+
+
+/**
+ * Test whether the given element has been shown or hidden via a call to
+ * {@link #setElementShown}.
+ *
+ * Note this is strictly a companion method for a call
+ * to {@link #setElementShown} and the same caveats apply; in particular, this
+ * method does not guarantee that the return value will be consistent with
+ * whether or not the element is actually visible.
+ *
+ * @param {Element} el The element to test.
+ * @return {boolean} Whether the element has been shown.
+ * @see #setElementShown
+ */
+goog.style.isElementShown = function(el) {
+  return el.style.display != 'none';
+};
+
+
+/**
+ * Installs the style sheet into the window that contains opt_node.  If
+ * opt_node is null, the main window is used.
+ * @param {!goog.html.SafeStyleSheet} safeStyleSheet The style sheet to install.
+ * @param {?Node=} opt_node Node whose parent document should have the
+ *     styles installed.
+ * @return {!HTMLStyleElement|!StyleSheet} In IE<11, a StyleSheet object with no
+ *     owning <style> tag (this is how IE creates style sheets).  In every other
+ *     browser, a <style> element with an attached style.  This doesn't return a
+ *     StyleSheet object so that setSafeStyleSheet can replace it (otherwise, if
+ *     you pass a StyleSheet to setSafeStyleSheet, it will make a new StyleSheet
+ *     and leave the original StyleSheet orphaned).
+ */
+goog.style.installSafeStyleSheet = function(safeStyleSheet, opt_node) {
+  var dh = goog.dom.getDomHelper(opt_node);
+
+  // IE < 11 requires createStyleSheet. Note that doc.createStyleSheet will be
+  // undefined as of IE 11.
+  var doc = dh.getDocument();
+  if (goog.userAgent.IE && doc.createStyleSheet) {
+    var styleSheet = doc.createStyleSheet();
+    goog.style.setSafeStyleSheet(styleSheet, safeStyleSheet);
+    return styleSheet;
+  } else {
+    var head = dh.getElementsByTagNameAndClass(goog.dom.TagName.HEAD)[0];
+
+    // In opera documents are not guaranteed to have a head element, thus we
+    // have to make sure one exists before using it.
+    if (!head) {
+      var body = dh.getElementsByTagNameAndClass(goog.dom.TagName.BODY)[0];
+      head = dh.createDom(goog.dom.TagName.HEAD);
+      body.parentNode.insertBefore(head, body);
+    }
+    var el = dh.createDom(goog.dom.TagName.STYLE);
+    // NOTE(vkarun): Setting styles after the style element has been appended
+    // to the head results in a nasty Webkit bug in certain scenarios. Please
+    // refer to https://bugs.webkit.org/show_bug.cgi?id=26307 for additional
+    // details.
+    goog.style.setSafeStyleSheet(el, safeStyleSheet);
+    dh.appendChild(head, el);
+    return el;
+  }
+};
+
+
+/**
+ * Removes the styles added by {@link #installStyles}.
+ * @param {!HTMLStyleElement|!StyleSheet} styleSheet The value returned by
+ *     {@link #installStyles}.
+ */
+goog.style.uninstallStyles = function(styleSheet) {
+  var node = styleSheet.ownerNode || styleSheet.owningElement ||
+      /** @type {Element} */ (styleSheet);
+  goog.dom.removeNode(node);
+};
+
+
+/**
+ * Sets the content of a style element.  The style element can be any valid
+ * style element.  This element will have its content completely replaced by
+ * the safeStyleSheet.
+ * @param {!HTMLStyleElement|!StyleSheet} element A <style> element, as returned
+ *     by installStyles (or a Stylesheet in IE<11).
+ * @param {!goog.html.SafeStyleSheet} safeStyleSheet The new content of the
+ *     stylesheet.
+ */
+goog.style.setSafeStyleSheet = function(element, safeStyleSheet) {
+  var stylesString = goog.html.SafeStyleSheet.unwrap(safeStyleSheet);
+  if (goog.userAgent.IE && goog.isDef(element.cssText)) {
+    // Adding the selectors individually caused the browser to hang if the
+    // selector was invalid or there were CSS comments.  Setting the cssText of
+    // the style node works fine and ignores CSS that IE doesn't understand.
+    // However IE >= 11 doesn't support cssText any more, so we make sure that
+    // cssText is a defined property and otherwise fall back to innerHTML.
+    element.cssText = stylesString;
+  } else {
+    // Setting textContent doesn't work in Safari, see b/29340337.
+    element.innerHTML = stylesString;
+  }
+};
+
+
+/**
+ * Sets 'white-space: pre-wrap' for a node (x-browser).
+ *
+ * There are as many ways of specifying pre-wrap as there are browsers.
+ *
+ * CSS3/IE8: white-space: pre-wrap;
+ * Mozilla:  white-space: -moz-pre-wrap;
+ * Opera:    white-space: -o-pre-wrap;
+ * IE6/7:    white-space: pre; word-wrap: break-word;
+ *
+ * @param {Element} el Element to enable pre-wrap for.
+ */
+goog.style.setPreWrap = function(el) {
+  var style = el.style;
+  if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('8')) {
+    style.whiteSpace = 'pre';
+    style.wordWrap = 'break-word';
+  } else if (goog.userAgent.GECKO) {
+    style.whiteSpace = '-moz-pre-wrap';
+  } else {
+    style.whiteSpace = 'pre-wrap';
+  }
+};
+
+
+/**
+ * Sets 'display: inline-block' for an element (cross-browser).
+ * @param {Element} el Element to which the inline-block display style is to be
+ *    applied.
+ * @see ../demos/inline_block_quirks.html
+ * @see ../demos/inline_block_standards.html
+ */
+goog.style.setInlineBlock = function(el) {
+  var style = el.style;
+  // Without position:relative, weirdness ensues.  Just accept it and move on.
+  style.position = 'relative';
+
+  if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('8')) {
+    // IE8 supports inline-block so fall through to the else
+    // Zoom:1 forces hasLayout, display:inline gives inline behavior.
+    style.zoom = '1';
+    style.display = 'inline';
+  } else {
+    // Opera, Webkit, and Safari seem to do OK with the standard inline-block
+    // style.
+    style.display = 'inline-block';
+  }
+};
+
+
+/**
+ * Returns true if the element is using right to left (rtl) direction.
+ * @param {Element} el  The element to test.
+ * @return {boolean} True for right to left, false for left to right.
+ */
+goog.style.isRightToLeft = function(el) {
+  return 'rtl' == goog.style.getStyle_(el, 'direction');
+};
+
+
+/**
+ * The CSS style property corresponding to an element being
+ * unselectable on the current browser platform (null if none).
+ * Opera and IE instead use a DOM attribute 'unselectable'. MS Edge uses
+ * the Webkit prefix.
+ * @type {?string}
+ * @private
+ */
+goog.style.unselectableStyle_ = goog.userAgent.GECKO ?
+    'MozUserSelect' :
+    goog.userAgent.WEBKIT || goog.userAgent.EDGE ? 'WebkitUserSelect' : null;
+
+
+/**
+ * Returns true if the element is set to be unselectable, false otherwise.
+ * Note that on some platforms (e.g. Mozilla), even if an element isn't set
+ * to be unselectable, it will behave as such if any of its ancestors is
+ * unselectable.
+ * @param {Element} el  Element to check.
+ * @return {boolean}  Whether the element is set to be unselectable.
+ */
+goog.style.isUnselectable = function(el) {
+  if (goog.style.unselectableStyle_) {
+    return el.style[goog.style.unselectableStyle_].toLowerCase() == 'none';
+  } else if (goog.userAgent.IE || goog.userAgent.OPERA) {
+    return el.getAttribute('unselectable') == 'on';
+  }
+  return false;
+};
+
+
+/**
+ * Makes the element and its descendants selectable or unselectable.  Note
+ * that on some platforms (e.g. Mozilla), even if an element isn't set to
+ * be unselectable, it will behave as such if any of its ancestors is
+ * unselectable.
+ * @param {Element} el  The element to alter.
+ * @param {boolean} unselectable  Whether the element and its descendants
+ *     should be made unselectable.
+ * @param {boolean=} opt_noRecurse  Whether to only alter the element's own
+ *     selectable state, and leave its descendants alone; defaults to false.
+ */
+goog.style.setUnselectable = function(el, unselectable, opt_noRecurse) {
+  // TODO(attila): Do we need all of TR_DomUtil.makeUnselectable() in Closure?
+  var descendants = !opt_noRecurse ? el.getElementsByTagName('*') : null;
+  var name = goog.style.unselectableStyle_;
+  if (name) {
+    // Add/remove the appropriate CSS style to/from the element and its
+    // descendants.
+    var value = unselectable ? 'none' : '';
+    // MathML elements do not have a style property. Verify before setting.
+    if (el.style) {
+      el.style[name] = value;
+    }
+    if (descendants) {
+      for (var i = 0, descendant; descendant = descendants[i]; i++) {
+        if (descendant.style) {
+          descendant.style[name] = value;
+        }
+      }
+    }
+  } else if (goog.userAgent.IE || goog.userAgent.OPERA) {
+    // Toggle the 'unselectable' attribute on the element and its descendants.
+    var value = unselectable ? 'on' : '';
+    el.setAttribute('unselectable', value);
+    if (descendants) {
+      for (var i = 0, descendant; descendant = descendants[i]; i++) {
+        descendant.setAttribute('unselectable', value);
+      }
+    }
+  }
+};
+
+
+/**
+ * Gets the border box size for an element.
+ * @param {Element} element  The element to get the size for.
+ * @return {!goog.math.Size} The border box size.
+ */
+goog.style.getBorderBoxSize = function(element) {
+  return new goog.math.Size(
+      /** @type {!HTMLElement} */ (element).offsetWidth,
+      /** @type {!HTMLElement} */ (element).offsetHeight);
+};
+
+
+/**
+ * Sets the border box size of an element. This is potentially expensive in IE
+ * if the document is CSS1Compat mode
+ * @param {Element} element  The element to set the size on.
+ * @param {goog.math.Size} size  The new size.
+ */
+goog.style.setBorderBoxSize = function(element, size) {
+  var doc = goog.dom.getOwnerDocument(element);
+  var isCss1CompatMode = goog.dom.getDomHelper(doc).isCss1CompatMode();
+
+  if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('10') &&
+      (!isCss1CompatMode || !goog.userAgent.isVersionOrHigher('8'))) {
+    var style = element.style;
+    if (isCss1CompatMode) {
+      var paddingBox = goog.style.getPaddingBox(element);
+      var borderBox = goog.style.getBorderBox(element);
+      style.pixelWidth = size.width - borderBox.left - paddingBox.left -
+          paddingBox.right - borderBox.right;
+      style.pixelHeight = size.height - borderBox.top - paddingBox.top -
+          paddingBox.bottom - borderBox.bottom;
+    } else {
+      style.pixelWidth = size.width;
+      style.pixelHeight = size.height;
+    }
+  } else {
+    goog.style.setBoxSizingSize_(element, size, 'border-box');
+  }
+};
+
+
+/**
+ * Gets the content box size for an element.  This is potentially expensive in
+ * all browsers.
+ * @param {Element} element  The element to get the size for.
+ * @return {!goog.math.Size} The content box size.
+ */
+goog.style.getContentBoxSize = function(element) {
+  var doc = goog.dom.getOwnerDocument(element);
+  var ieCurrentStyle = goog.userAgent.IE && element.currentStyle;
+  if (ieCurrentStyle && goog.dom.getDomHelper(doc).isCss1CompatMode() &&
+      ieCurrentStyle.width != 'auto' && ieCurrentStyle.height != 'auto' &&
+      !ieCurrentStyle.boxSizing) {
+    // If IE in CSS1Compat mode than just use the width and height.
+    // If we have a boxSizing then fall back on measuring the borders etc.
+    var width = goog.style.getIePixelValue_(
+        element, /** @type {string} */ (ieCurrentStyle.width), 'width',
+        'pixelWidth');
+    var height = goog.style.getIePixelValue_(
+        element, /** @type {string} */ (ieCurrentStyle.height), 'height',
+        'pixelHeight');
+    return new goog.math.Size(width, height);
+  } else {
+    var borderBoxSize = goog.style.getBorderBoxSize(element);
+    var paddingBox = goog.style.getPaddingBox(element);
+    var borderBox = goog.style.getBorderBox(element);
+    return new goog.math.Size(
+        borderBoxSize.width - borderBox.left - paddingBox.left -
+            paddingBox.right - borderBox.right,
+        borderBoxSize.height - borderBox.top - paddingBox.top -
+            paddingBox.bottom - borderBox.bottom);
+  }
+};
+
+
+/**
+ * Sets the content box size of an element. This is potentially expensive in IE
+ * if the document is BackCompat mode.
+ * @param {Element} element  The element to set the size on.
+ * @param {goog.math.Size} size  The new size.
+ */
+goog.style.setContentBoxSize = function(element, size) {
+  var doc = goog.dom.getOwnerDocument(element);
+  var isCss1CompatMode = goog.dom.getDomHelper(doc).isCss1CompatMode();
+  if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('10') &&
+      (!isCss1CompatMode || !goog.userAgent.isVersionOrHigher('8'))) {
+    var style = element.style;
+    if (isCss1CompatMode) {
+      style.pixelWidth = size.width;
+      style.pixelHeight = size.height;
+    } else {
+      var paddingBox = goog.style.getPaddingBox(element);
+      var borderBox = goog.style.getBorderBox(element);
+      style.pixelWidth = size.width + borderBox.left + paddingBox.left +
+          paddingBox.right + borderBox.right;
+      style.pixelHeight = size.height + borderBox.top + paddingBox.top +
+          paddingBox.bottom + borderBox.bottom;
+    }
+  } else {
+    goog.style.setBoxSizingSize_(element, size, 'content-box');
+  }
+};
+
+
+/**
+ * Helper function that sets the box sizing as well as the width and height
+ * @param {Element} element  The element to set the size on.
+ * @param {goog.math.Size} size  The new size to set.
+ * @param {string} boxSizing  The box-sizing value.
+ * @private
+ */
+goog.style.setBoxSizingSize_ = function(element, size, boxSizing) {
+  var style = element.style;
+  if (goog.userAgent.GECKO) {
+    style.MozBoxSizing = boxSizing;
+  } else if (goog.userAgent.WEBKIT) {
+    style.WebkitBoxSizing = boxSizing;
+  } else {
+    // Includes IE8 and Opera 9.50+
+    style.boxSizing = boxSizing;
+  }
+
+  // Setting this to a negative value will throw an exception on IE
+  // (and doesn't do anything different than setting it to 0).
+  style.width = Math.max(size.width, 0) + 'px';
+  style.height = Math.max(size.height, 0) + 'px';
+};
+
+
+/**
+ * IE specific function that converts a non pixel unit to pixels.
+ * @param {Element} element  The element to convert the value for.
+ * @param {string} value  The current value as a string. The value must not be
+ *     ''.
+ * @param {string} name  The CSS property name to use for the converstion. This
+ *     should be 'left', 'top', 'width' or 'height'.
+ * @param {string} pixelName  The CSS pixel property name to use to get the
+ *     value in pixels.
+ * @return {number} The value in pixels.
+ * @private
+ */
+goog.style.getIePixelValue_ = function(element, value, name, pixelName) {
+  // Try if we already have a pixel value. IE does not do half pixels so we
+  // only check if it matches a number followed by 'px'.
+  if (/^\d+px?$/.test(value)) {
+    return parseInt(value, 10);
+  } else {
+    var oldStyleValue = element.style[name];
+    var oldRuntimeValue = element.runtimeStyle[name];
+    // set runtime style to prevent changes
+    element.runtimeStyle[name] = element.currentStyle[name];
+    element.style[name] = value;
+    var pixelValue = element.style[pixelName];
+    // restore
+    element.style[name] = oldStyleValue;
+    element.runtimeStyle[name] = oldRuntimeValue;
+    return +pixelValue;
+  }
+};
+
+
+/**
+ * Helper function for getting the pixel padding or margin for IE.
+ * @param {Element} element  The element to get the padding for.
+ * @param {string} propName  The property name.
+ * @return {number} The pixel padding.
+ * @private
+ */
+goog.style.getIePixelDistance_ = function(element, propName) {
+  var value = goog.style.getCascadedStyle(element, propName);
+  return value ?
+      goog.style.getIePixelValue_(element, value, 'left', 'pixelLeft') :
+      0;
+};
+
+
+/**
+ * Gets the computed paddings or margins (on all sides) in pixels.
+ * @param {Element} element  The element to get the padding for.
+ * @param {string} stylePrefix  Pass 'padding' to retrieve the padding box,
+ *     or 'margin' to retrieve the margin box.
+ * @return {!goog.math.Box} The computed paddings or margins.
+ * @private
+ */
+goog.style.getBox_ = function(element, stylePrefix) {
+  if (goog.userAgent.IE) {
+    var left = goog.style.getIePixelDistance_(element, stylePrefix + 'Left');
+    var right = goog.style.getIePixelDistance_(element, stylePrefix + 'Right');
+    var top = goog.style.getIePixelDistance_(element, stylePrefix + 'Top');
+    var bottom =
+        goog.style.getIePixelDistance_(element, stylePrefix + 'Bottom');
+    return new goog.math.Box(top, right, bottom, left);
+  } else {
+    // On non-IE browsers, getComputedStyle is always non-null.
+    var left = goog.style.getComputedStyle(element, stylePrefix + 'Left');
+    var right = goog.style.getComputedStyle(element, stylePrefix + 'Right');
+    var top = goog.style.getComputedStyle(element, stylePrefix + 'Top');
+    var bottom = goog.style.getComputedStyle(element, stylePrefix + 'Bottom');
+
+    // NOTE(arv): Gecko can return floating point numbers for the computed
+    // style values.
+    return new goog.math.Box(
+        parseFloat(top), parseFloat(right), parseFloat(bottom),
+        parseFloat(left));
+  }
+};
+
+
+/**
+ * Gets the computed paddings (on all sides) in pixels.
+ * @param {Element} element  The element to get the padding for.
+ * @return {!goog.math.Box} The computed paddings.
+ */
+goog.style.getPaddingBox = function(element) {
+  return goog.style.getBox_(element, 'padding');
+};
+
+
+/**
+ * Gets the computed margins (on all sides) in pixels.
+ * @param {Element} element  The element to get the margins for.
+ * @return {!goog.math.Box} The computed margins.
+ */
+goog.style.getMarginBox = function(element) {
+  return goog.style.getBox_(element, 'margin');
+};
+
+
+/**
+ * A map used to map the border width keywords to a pixel width.
+ * @type {!Object}
+ * @private
+ */
+goog.style.ieBorderWidthKeywords_ = {
+  'thin': 2,
+  'medium': 4,
+  'thick': 6
+};
+
+
+/**
+ * Helper function for IE to get the pixel border.
+ * @param {Element} element  The element to get the pixel border for.
+ * @param {string} prop  The part of the property name.
+ * @return {number} The value in pixels.
+ * @private
+ */
+goog.style.getIePixelBorder_ = function(element, prop) {
+  if (goog.style.getCascadedStyle(element, prop + 'Style') == 'none') {
+    return 0;
+  }
+  var width = goog.style.getCascadedStyle(element, prop + 'Width');
+  if (width in goog.style.ieBorderWidthKeywords_) {
+    return goog.style.ieBorderWidthKeywords_[width];
+  }
+  return goog.style.getIePixelValue_(element, width, 'left', 'pixelLeft');
+};
+
+
+/**
+ * Gets the computed border widths (on all sides) in pixels
+ * @param {Element} element  The element to get the border widths for.
+ * @return {!goog.math.Box} The computed border widths.
+ */
+goog.style.getBorderBox = function(element) {
+  if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
+    var left = goog.style.getIePixelBorder_(element, 'borderLeft');
+    var right = goog.style.getIePixelBorder_(element, 'borderRight');
+    var top = goog.style.getIePixelBorder_(element, 'borderTop');
+    var bottom = goog.style.getIePixelBorder_(element, 'borderBottom');
+    return new goog.math.Box(top, right, bottom, left);
+  } else {
+    // On non-IE browsers, getComputedStyle is always non-null.
+    var left = goog.style.getComputedStyle(element, 'borderLeftWidth');
+    var right = goog.style.getComputedStyle(element, 'borderRightWidth');
+    var top = goog.style.getComputedStyle(element, 'borderTopWidth');
+    var bottom = goog.style.getComputedStyle(element, 'borderBottomWidth');
+
+    return new goog.math.Box(
+        parseFloat(top), parseFloat(right), parseFloat(bottom),
+        parseFloat(left));
+  }
+};
+
+
+/**
+ * Returns the font face applied to a given node. Opera and IE should return
+ * the font actually displayed. Firefox returns the author's most-preferred
+ * font (whether the browser is capable of displaying it or not.)
+ * @param {Element} el  The element whose font family is returned.
+ * @return {string} The font family applied to el.
+ */
+goog.style.getFontFamily = function(el) {
+  var doc = goog.dom.getOwnerDocument(el);
+  var font = '';
+  // The moveToElementText method from the TextRange only works if the element
+  // is attached to the owner document.
+  if (doc.body.createTextRange && goog.dom.contains(doc, el)) {
+    var range = doc.body.createTextRange();
+    range.moveToElementText(el);
+
+    try {
+      font = range.queryCommandValue('FontName');
+    } catch (e) {
+      // This is a workaround for a awkward exception.
+      // On some IE, there is an exception coming from it.
+      // The error description from this exception is:
+      // This window has already been registered as a drop target
+      // This is bogus description, likely due to a bug in ie.
+      font = '';
+    }
+  }
+  if (!font) {
+    // Note if for some reason IE can't derive FontName with a TextRange, we
+    // fallback to using currentStyle
+    font = goog.style.getStyle_(el, 'fontFamily');
+  }
+
+  // Firefox returns the applied font-family string (author's list of
+  // preferred fonts.) We want to return the most-preferred font, in lieu of
+  // the *actually* applied font.
+  var fontsArray = font.split(',');
+  if (fontsArray.length > 1) font = fontsArray[0];
+
+  // Sanitize for x-browser consistency:
+  // Strip quotes because browsers aren't consistent with how they're
+  // applied; Opera always encloses, Firefox sometimes, and IE never.
+  return goog.string.stripQuotes(font, '"\'');
+};
+
+
+/**
+ * Regular expression used for getLengthUnits.
+ * @type {RegExp}
+ * @private
+ */
+goog.style.lengthUnitRegex_ = /[^\d]+$/;
+
+
+/**
+ * Returns the units used for a CSS length measurement.
+ * @param {string} value  A CSS length quantity.
+ * @return {?string} The units of measurement.
+ */
+goog.style.getLengthUnits = function(value) {
+  var units = value.match(goog.style.lengthUnitRegex_);
+  return units && units[0] || null;
+};
+
+
+/**
+ * Map of absolute CSS length units
+ * @type {!Object}
+ * @private
+ */
+goog.style.ABSOLUTE_CSS_LENGTH_UNITS_ = {
+  'cm': 1,
+  'in': 1,
+  'mm': 1,
+  'pc': 1,
+  'pt': 1
+};
+
+
+/**
+ * Map of relative CSS length units that can be accurately converted to px
+ * font-size values using getIePixelValue_. Only units that are defined in
+ * relation to a font size are convertible (%, small, etc. are not).
+ * @type {!Object}
+ * @private
+ */
+goog.style.CONVERTIBLE_RELATIVE_CSS_UNITS_ = {
+  'em': 1,
+  'ex': 1
+};
+
+
+/**
+ * Returns the font size, in pixels, of text in an element.
+ * @param {Element} el  The element whose font size is returned.
+ * @return {number} The font size (in pixels).
+ */
+goog.style.getFontSize = function(el) {
+  var fontSize = goog.style.getStyle_(el, 'fontSize');
+  var sizeUnits = goog.style.getLengthUnits(fontSize);
+  if (fontSize && 'px' == sizeUnits) {
+    // NOTE(nathanl): This could be parseFloat instead, but IE doesn't return
+    // decimal fractions in getStyle_ and Firefox reports the fractions, but
+    // ignores them when rendering. Interestingly enough, when we force the
+    // issue and size something to e.g., 50% of 25px, the browsers round in
+    // opposite directions with Firefox reporting 12px and IE 13px. I punt.
+    return parseInt(fontSize, 10);
+  }
+
+  // In IE, we can convert absolute length units to a px value using
+  // goog.style.getIePixelValue_. Units defined in relation to a font size
+  // (em, ex) are applied relative to the element's parentNode and can also
+  // be converted.
+  if (goog.userAgent.IE) {
+    if (String(sizeUnits) in goog.style.ABSOLUTE_CSS_LENGTH_UNITS_) {
+      return goog.style.getIePixelValue_(el, fontSize, 'left', 'pixelLeft');
+    } else if (
+        el.parentNode && el.parentNode.nodeType == goog.dom.NodeType.ELEMENT &&
+        String(sizeUnits) in goog.style.CONVERTIBLE_RELATIVE_CSS_UNITS_) {
+      // Check the parent size - if it is the same it means the relative size
+      // value is inherited and we therefore don't want to count it twice.  If
+      // it is different, this element either has explicit style or has a CSS
+      // rule applying to it.
+      var parentElement = /** @type {!Element} */ (el.parentNode);
+      var parentSize = goog.style.getStyle_(parentElement, 'fontSize');
+      return goog.style.getIePixelValue_(
+          parentElement, fontSize == parentSize ? '1em' : fontSize, 'left',
+          'pixelLeft');
+    }
+  }
+
+  // Sometimes we can't cleanly find the font size (some units relative to a
+  // node's parent's font size are difficult: %, smaller et al), so we create
+  // an invisible, absolutely-positioned span sized to be the height of an 'M'
+  // rendered in its parent's (i.e., our target element's) font size. This is
+  // the definition of CSS's font size attribute.
+  var sizeElement = goog.dom.createDom(goog.dom.TagName.SPAN, {
+    'style': 'visibility:hidden;position:absolute;' +
+        'line-height:0;padding:0;margin:0;border:0;height:1em;'
+  });
+  goog.dom.appendChild(el, sizeElement);
+  fontSize = sizeElement.offsetHeight;
+  goog.dom.removeNode(sizeElement);
+
+  return fontSize;
+};
+
+
+/**
+ * Parses a style attribute value.  Converts CSS property names to camel case.
+ * @param {string} value The style attribute value.
+ * @return {!Object} Map of CSS properties to string values.
+ */
+goog.style.parseStyleAttribute = function(value) {
+  var result = {};
+  goog.array.forEach(value.split(/\s*;\s*/), function(pair) {
+    var keyValue = pair.match(/\s*([\w-]+)\s*\:(.+)/);
+    if (keyValue) {
+      var styleName = keyValue[1];
+      var styleValue = goog.string.trim(keyValue[2]);
+      result[goog.string.toCamelCase(styleName.toLowerCase())] = styleValue;
+    }
+  });
+  return result;
+};
+
+
+/**
+ * Reverse of parseStyleAttribute; that is, takes a style object and returns the
+ * corresponding attribute value.  Converts camel case property names to proper
+ * CSS selector names.
+ * @param {Object} obj Map of CSS properties to values.
+ * @return {string} The style attribute value.
+ */
+goog.style.toStyleAttribute = function(obj) {
+  var buffer = [];
+  goog.object.forEach(obj, function(value, key) {
+    buffer.push(goog.string.toSelectorCase(key), ':', value, ';');
+  });
+  return buffer.join('');
+};
+
+
+/**
+ * Sets CSS float property on an element.
+ * @param {Element} el The element to set float property on.
+ * @param {string} value The value of float CSS property to set on this element.
+ */
+goog.style.setFloat = function(el, value) {
+  el.style[goog.userAgent.IE ? 'styleFloat' : 'cssFloat'] = value;
+};
+
+
+/**
+ * Gets value of explicitly-set float CSS property on an element.
+ * @param {Element} el The element to get float property of.
+ * @return {string} The value of explicitly-set float CSS property on this
+ *     element.
+ */
+goog.style.getFloat = function(el) {
+  return el.style[goog.userAgent.IE ? 'styleFloat' : 'cssFloat'] || '';
+};
+
+
+/**
+ * Returns the scroll bar width (represents the width of both horizontal
+ * and vertical scroll).
+ *
+ * @param {string=} opt_className An optional class name (or names) to apply
+ *     to the invisible div created to measure the scrollbar. This is necessary
+ *     if some scrollbars are styled differently than others.
+ * @return {number} The scroll bar width in px.
+ */
+goog.style.getScrollbarWidth = function(opt_className) {
+  // Add two hidden divs.  The child div is larger than the parent and
+  // forces scrollbars to appear on it.
+  // Using overflow:scroll does not work consistently with scrollbars that
+  // are styled with ::-webkit-scrollbar.
+  var outerDiv = goog.dom.createElement(goog.dom.TagName.DIV);
+  if (opt_className) {
+    outerDiv.className = opt_className;
+  }
+  outerDiv.style.cssText = 'overflow:auto;' +
+      'position:absolute;top:0;width:100px;height:100px';
+  var innerDiv = goog.dom.createElement(goog.dom.TagName.DIV);
+  goog.style.setSize(innerDiv, '200px', '200px');
+  outerDiv.appendChild(innerDiv);
+  goog.dom.appendChild(goog.dom.getDocument().body, outerDiv);
+  var width = outerDiv.offsetWidth - outerDiv.clientWidth;
+  goog.dom.removeNode(outerDiv);
+  return width;
+};
+
+
+/**
+ * Regular expression to extract x and y translation components from a CSS
+ * transform Matrix representation.
+ *
+ * @type {!RegExp}
+ * @const
+ * @private
+ */
+goog.style.MATRIX_TRANSLATION_REGEX_ = new RegExp(
+    'matrix\\([0-9\\.\\-]+, [0-9\\.\\-]+, ' +
+    '[0-9\\.\\-]+, [0-9\\.\\-]+, ' +
+    '([0-9\\.\\-]+)p?x?, ([0-9\\.\\-]+)p?x?\\)');
+
+
+/**
+ * Returns the x,y translation component of any CSS transforms applied to the
+ * element, in pixels.
+ *
+ * @param {!Element} element The element to get the translation of.
+ * @return {!goog.math.Coordinate} The CSS translation of the element in px.
+ */
+goog.style.getCssTranslation = function(element) {
+  var transform = goog.style.getComputedTransform(element);
+  if (!transform) {
+    return new goog.math.Coordinate(0, 0);
+  }
+  var matches = transform.match(goog.style.MATRIX_TRANSLATION_REGEX_);
+  if (!matches) {
+    return new goog.math.Coordinate(0, 0);
+  }
+  return new goog.math.Coordinate(
+      parseFloat(matches[1]), parseFloat(matches[2]));
+};
diff --git a/third_party/ink/closure/ui/component.js b/third_party/ink/closure/ui/component.js
new file mode 100644
index 0000000..f4f7737a
--- /dev/null
+++ b/third_party/ink/closure/ui/component.js
@@ -0,0 +1,1305 @@
+// Copyright 2007 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 Abstract class for all UI components. This defines the standard
+ * design pattern that all UI components should follow.
+ *
+ * @author pupius@google.com (Daniel Pupius)
+ * @author ssaviano@google.com (Steven Saviano)
+ * @author baker@google.com (Greg Baker)
+ * @author attila@google.com (Attila Bodis)
+ * @see ../demos/samplecomponent.html
+ * @see http://code.google.com/p/closure-library/wiki/IntroToComponents
+ */
+
+goog.provide('goog.ui.Component');
+goog.provide('goog.ui.Component.Error');
+goog.provide('goog.ui.Component.EventType');
+goog.provide('goog.ui.Component.State');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.dom');
+goog.require('goog.dom.NodeType');
+goog.require('goog.dom.TagName');
+goog.require('goog.events.EventHandler');
+goog.require('goog.events.EventTarget');
+goog.require('goog.object');
+goog.require('goog.style');
+goog.require('goog.ui.IdGenerator');
+
+
+
+/**
+ * Default implementation of UI component.
+ *
+ * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
+ * @constructor
+ * @extends {goog.events.EventTarget}
+ * @suppress {underscore}
+ */
+goog.ui.Component = function(opt_domHelper) {
+  goog.events.EventTarget.call(this);
+  /**
+   * DomHelper used to interact with the document, allowing components to be
+   * created in a different window.
+   * @protected {!goog.dom.DomHelper}
+   * @suppress {underscore|visibility}
+   */
+  this.dom_ = opt_domHelper || goog.dom.getDomHelper();
+
+  /**
+   * Whether the component is rendered right-to-left.  Right-to-left is set
+   * lazily when {@link #isRightToLeft} is called the first time, unless it has
+   * been set by calling {@link #setRightToLeft} explicitly.
+   * @private {?boolean}
+   */
+  this.rightToLeft_ = goog.ui.Component.defaultRightToLeft_;
+
+  /**
+   * Unique ID of the component, lazily initialized in {@link
+   * goog.ui.Component#getId} if needed.  This property is strictly private and
+   * must not be accessed directly outside of this class!
+   * @private {?string}
+   */
+  this.id_ = null;
+
+  /**
+   * Whether the component is in the document.
+   * @private {boolean}
+   */
+  this.inDocument_ = false;
+
+  // TODO(attila): Stop referring to this private field in subclasses.
+  /**
+   * The DOM element for the component.
+   * @private {Element}
+   */
+  this.element_ = null;
+
+  /**
+   * Event handler.
+   * TODO(pallosp): rename it to handler_ after all component subclasses in
+   * inside Google have been cleaned up.
+   * Code search: http://go/component_code_search
+   * @private {goog.events.EventHandler|undefined}
+   */
+  this.googUiComponentHandler_ = void 0;
+
+  /**
+   * Arbitrary data object associated with the component.  Such as meta-data.
+   * @private {*}
+   */
+  this.model_ = null;
+
+  /**
+   * Parent component to which events will be propagated.  This property is
+   * strictly private and must not be accessed directly outside of this class!
+   * @private {goog.ui.Component?}
+   */
+  this.parent_ = null;
+
+  /**
+   * Array of child components.  Lazily initialized on first use.  Must be kept
+   * in sync with {@code childIndex_}.  This property is strictly private and
+   * must not be accessed directly outside of this class!
+   * @private {Array<goog.ui.Component>?}
+   */
+  this.children_ = null;
+
+  /**
+   * Map of child component IDs to child components.  Used for constant-time
+   * random access to child components by ID.  Lazily initialized on first use.
+   * Must be kept in sync with {@code children_}.  This property is strictly
+   * private and must not be accessed directly outside of this class!
+   *
+   * We use a plain Object, not a {@link goog.structs.Map}, for simplicity.
+   * This means components can't have children with IDs such as 'constructor' or
+   * 'valueOf', but this shouldn't really be an issue in practice, and if it is,
+   * we can always fix it later without changing the API.
+   *
+   * @private {Object}
+   */
+  this.childIndex_ = null;
+
+  /**
+   * Flag used to keep track of whether a component decorated an already
+   * existing element or whether it created the DOM itself.
+   *
+   * If an element is decorated, dispose will leave the node in the document.
+   * It is up to the app to remove the node.
+   *
+   * If an element was rendered, dispose will remove the node automatically.
+   *
+   * @private {boolean}
+   */
+  this.wasDecorated_ = false;
+};
+goog.inherits(goog.ui.Component, goog.events.EventTarget);
+
+
+/**
+ * @define {boolean} Whether to support calling decorate with an element that is
+ *     not yet in the document. If true, we check if the element is in the
+ *     document, and avoid calling enterDocument if it isn't. If false, we
+ *     maintain legacy behavior (always call enterDocument from decorate).
+ */
+goog.define('goog.ui.Component.ALLOW_DETACHED_DECORATION', false);
+
+
+/**
+ * Generator for unique IDs.
+ * @type {goog.ui.IdGenerator}
+ * @private
+ */
+goog.ui.Component.prototype.idGenerator_ = goog.ui.IdGenerator.getInstance();
+
+
+// TODO(gboyer): See if we can remove this and just check goog.i18n.bidi.IS_RTL.
+/**
+ * @define {number} Defines the default BIDI directionality.
+ *     0: Unknown.
+ *     1: Left-to-right.
+ *     -1: Right-to-left.
+ */
+goog.define('goog.ui.Component.DEFAULT_BIDI_DIR', 0);
+
+
+/**
+ * The default right to left value.
+ * @type {?boolean}
+ * @private
+ */
+goog.ui.Component.defaultRightToLeft_ =
+    (goog.ui.Component.DEFAULT_BIDI_DIR == 1) ?
+    false :
+    (goog.ui.Component.DEFAULT_BIDI_DIR == -1) ? true : null;
+
+
+/**
+ * Common events fired by components so that event propagation is useful.  Not
+ * all components are expected to dispatch or listen for all event types.
+ * Events dispatched before a state transition should be cancelable to prevent
+ * the corresponding state change.
+ * @enum {string}
+ */
+goog.ui.Component.EventType = {
+  /** Dispatched before the component becomes visible. */
+  BEFORE_SHOW: 'beforeshow',
+
+  /**
+   * Dispatched after the component becomes visible.
+   * NOTE(bloom): For goog.ui.Container, this actually fires before containers
+   * are shown.  Use goog.ui.Container.EventType.AFTER_SHOW if you want an event
+   * that fires after a goog.ui.Container is shown.
+   */
+  SHOW: 'show',
+
+  /** Dispatched before the component becomes hidden. */
+  HIDE: 'hide',
+
+  /** Dispatched before the component becomes disabled. */
+  DISABLE: 'disable',
+
+  /** Dispatched before the component becomes enabled. */
+  ENABLE: 'enable',
+
+  /** Dispatched before the component becomes highlighted. */
+  HIGHLIGHT: 'highlight',
+
+  /** Dispatched before the component becomes un-highlighted. */
+  UNHIGHLIGHT: 'unhighlight',
+
+  /** Dispatched before the component becomes activated. */
+  ACTIVATE: 'activate',
+
+  /** Dispatched before the component becomes deactivated. */
+  DEACTIVATE: 'deactivate',
+
+  /** Dispatched before the component becomes selected. */
+  SELECT: 'select',
+
+  /** Dispatched before the component becomes un-selected. */
+  UNSELECT: 'unselect',
+
+  /** Dispatched before a component becomes checked. */
+  CHECK: 'check',
+
+  /** Dispatched before a component becomes un-checked. */
+  UNCHECK: 'uncheck',
+
+  /** Dispatched before a component becomes focused. */
+  FOCUS: 'focus',
+
+  /** Dispatched before a component becomes blurred. */
+  BLUR: 'blur',
+
+  /** Dispatched before a component is opened (expanded). */
+  OPEN: 'open',
+
+  /** Dispatched before a component is closed (collapsed). */
+  CLOSE: 'close',
+
+  /** Dispatched after a component is moused over. */
+  ENTER: 'enter',
+
+  /** Dispatched after a component is moused out of. */
+  LEAVE: 'leave',
+
+  /** Dispatched after the user activates the component. */
+  ACTION: 'action',
+
+  /** Dispatched after the external-facing state of a component is changed. */
+  CHANGE: 'change'
+};
+
+
+/**
+ * Errors thrown by the component.
+ * @enum {string}
+ */
+goog.ui.Component.Error = {
+  /**
+   * Error when a method is not supported.
+   */
+  NOT_SUPPORTED: 'Method not supported',
+
+  /**
+   * Error when the given element can not be decorated.
+   */
+  DECORATE_INVALID: 'Invalid element to decorate',
+
+  /**
+   * Error when the component is already rendered and another render attempt is
+   * made.
+   */
+  ALREADY_RENDERED: 'Component already rendered',
+
+  /**
+   * Error when an attempt is made to set the parent of a component in a way
+   * that would result in an inconsistent object graph.
+   */
+  PARENT_UNABLE_TO_BE_SET: 'Unable to set parent component',
+
+  /**
+   * Error when an attempt is made to add a child component at an out-of-bounds
+   * index.  We don't support sparse child arrays.
+   */
+  CHILD_INDEX_OUT_OF_BOUNDS: 'Child component index out of bounds',
+
+  /**
+   * Error when an attempt is made to remove a child component from a component
+   * other than its parent.
+   */
+  NOT_OUR_CHILD: 'Child is not in parent component',
+
+  /**
+   * Error when an operation requiring DOM interaction is made when the
+   * component is not in the document
+   */
+  NOT_IN_DOCUMENT: 'Operation not supported while component is not in document',
+
+  /**
+   * Error when an invalid component state is encountered.
+   */
+  STATE_INVALID: 'Invalid component state'
+};
+
+
+/**
+ * Common component states.  Components may have distinct appearance depending
+ * on what state(s) apply to them.  Not all components are expected to support
+ * all states.
+ * @enum {number}
+ */
+goog.ui.Component.State = {
+  /**
+   * Union of all supported component states.
+   */
+  ALL: 0xFF,
+
+  /**
+   * Component is disabled.
+   * @see goog.ui.Component.EventType.DISABLE
+   * @see goog.ui.Component.EventType.ENABLE
+   */
+  DISABLED: 0x01,
+
+  /**
+   * Component is highlighted.
+   * @see goog.ui.Component.EventType.HIGHLIGHT
+   * @see goog.ui.Component.EventType.UNHIGHLIGHT
+   */
+  HOVER: 0x02,
+
+  /**
+   * Component is active (or "pressed").
+   * @see goog.ui.Component.EventType.ACTIVATE
+   * @see goog.ui.Component.EventType.DEACTIVATE
+   */
+  ACTIVE: 0x04,
+
+  /**
+   * Component is selected.
+   * @see goog.ui.Component.EventType.SELECT
+   * @see goog.ui.Component.EventType.UNSELECT
+   */
+  SELECTED: 0x08,
+
+  /**
+   * Component is checked.
+   * @see goog.ui.Component.EventType.CHECK
+   * @see goog.ui.Component.EventType.UNCHECK
+   */
+  CHECKED: 0x10,
+
+  /**
+   * Component has focus.
+   * @see goog.ui.Component.EventType.FOCUS
+   * @see goog.ui.Component.EventType.BLUR
+   */
+  FOCUSED: 0x20,
+
+  /**
+   * Component is opened (expanded).  Applies to tree nodes, menu buttons,
+   * submenus, zippys (zippies?), etc.
+   * @see goog.ui.Component.EventType.OPEN
+   * @see goog.ui.Component.EventType.CLOSE
+   */
+  OPENED: 0x40
+};
+
+
+/**
+ * Static helper method; returns the type of event components are expected to
+ * dispatch when transitioning to or from the given state.
+ * @param {goog.ui.Component.State} state State to/from which the component
+ *     is transitioning.
+ * @param {boolean} isEntering Whether the component is entering or leaving the
+ *     state.
+ * @return {goog.ui.Component.EventType} Event type to dispatch.
+ */
+goog.ui.Component.getStateTransitionEvent = function(state, isEntering) {
+  switch (state) {
+    case goog.ui.Component.State.DISABLED:
+      return isEntering ? goog.ui.Component.EventType.DISABLE :
+                          goog.ui.Component.EventType.ENABLE;
+    case goog.ui.Component.State.HOVER:
+      return isEntering ? goog.ui.Component.EventType.HIGHLIGHT :
+                          goog.ui.Component.EventType.UNHIGHLIGHT;
+    case goog.ui.Component.State.ACTIVE:
+      return isEntering ? goog.ui.Component.EventType.ACTIVATE :
+                          goog.ui.Component.EventType.DEACTIVATE;
+    case goog.ui.Component.State.SELECTED:
+      return isEntering ? goog.ui.Component.EventType.SELECT :
+                          goog.ui.Component.EventType.UNSELECT;
+    case goog.ui.Component.State.CHECKED:
+      return isEntering ? goog.ui.Component.EventType.CHECK :
+                          goog.ui.Component.EventType.UNCHECK;
+    case goog.ui.Component.State.FOCUSED:
+      return isEntering ? goog.ui.Component.EventType.FOCUS :
+                          goog.ui.Component.EventType.BLUR;
+    case goog.ui.Component.State.OPENED:
+      return isEntering ? goog.ui.Component.EventType.OPEN :
+                          goog.ui.Component.EventType.CLOSE;
+    default:
+      // Fall through.
+  }
+
+  // Invalid state.
+  throw new Error(goog.ui.Component.Error.STATE_INVALID);
+};
+
+
+/**
+ * Set the default right-to-left value. This causes all component's created from
+ * this point forward to have the given value. This is useful for cases where
+ * a given page is always in one directionality, avoiding unnecessary
+ * right to left determinations.
+ * @param {?boolean} rightToLeft Whether the components should be rendered
+ *     right-to-left. Null iff components should determine their directionality.
+ */
+goog.ui.Component.setDefaultRightToLeft = function(rightToLeft) {
+  goog.ui.Component.defaultRightToLeft_ = rightToLeft;
+};
+
+
+/**
+ * Gets the unique ID for the instance of this component.  If the instance
+ * doesn't already have an ID, generates one on the fly.
+ * @return {string} Unique component ID.
+ */
+goog.ui.Component.prototype.getId = function() {
+  return this.id_ || (this.id_ = this.idGenerator_.getNextUniqueId());
+};
+
+
+/**
+ * Assigns an ID to this component instance.  It is the caller's responsibility
+ * to guarantee that the ID is unique.  If the component is a child of a parent
+ * component, then the parent component's child index is updated to reflect the
+ * new ID; this may throw an error if the parent already has a child with an ID
+ * that conflicts with the new ID.
+ * @param {string} id Unique component ID.
+ */
+goog.ui.Component.prototype.setId = function(id) {
+  if (this.parent_ && this.parent_.childIndex_) {
+    // Update the parent's child index.
+    goog.object.remove(this.parent_.childIndex_, this.id_);
+    goog.object.add(this.parent_.childIndex_, id, this);
+  }
+
+  // Update the component ID.
+  this.id_ = id;
+};
+
+
+/**
+ * Gets the component's element.
+ * @return {Element} The element for the component.
+ */
+goog.ui.Component.prototype.getElement = function() {
+  return this.element_;
+};
+
+
+/**
+ * Gets the component's element. This differs from getElement in that
+ * it assumes that the element exists (i.e. the component has been
+ * rendered/decorated) and will cause an assertion error otherwise (if
+ * assertion is enabled).
+ * @return {!Element} The element for the component.
+ */
+goog.ui.Component.prototype.getElementStrict = function() {
+  var el = this.element_;
+  goog.asserts.assert(
+      el, 'Can not call getElementStrict before rendering/decorating.');
+  return el;
+};
+
+
+/**
+ * Sets the component's root element to the given element.  Considered
+ * protected and final.
+ *
+ * This should generally only be called during createDom. Setting the element
+ * does not actually change which element is rendered, only the element that is
+ * associated with this UI component.
+ *
+ * This should only be used by subclasses and its associated renderers.
+ *
+ * @param {Element} element Root element for the component.
+ */
+goog.ui.Component.prototype.setElementInternal = function(element) {
+  this.element_ = element;
+};
+
+
+/**
+ * Returns an array of all the elements in this component's DOM with the
+ * provided className.
+ * @param {string} className The name of the class to look for.
+ * @return {!IArrayLike<!Element>} The items found with the class name provided.
+ */
+goog.ui.Component.prototype.getElementsByClass = function(className) {
+  return this.element_ ?
+      this.dom_.getElementsByClass(className, this.element_) :
+      [];
+};
+
+
+/**
+ * Returns the first element in this component's DOM with the provided
+ * className.
+ * @param {string} className The name of the class to look for.
+ * @return {Element} The first item with the class name provided.
+ */
+goog.ui.Component.prototype.getElementByClass = function(className) {
+  return this.element_ ? this.dom_.getElementByClass(className, this.element_) :
+                         null;
+};
+
+
+/**
+ * Similar to {@code getElementByClass} except that it expects the
+ * element to be present in the dom thus returning a required value. Otherwise,
+ * will assert.
+ * @param {string} className The name of the class to look for.
+ * @return {!Element} The first item with the class name provided.
+ */
+goog.ui.Component.prototype.getRequiredElementByClass = function(className) {
+  var el = this.getElementByClass(className);
+  goog.asserts.assert(
+      el, 'Expected element in component with class: %s', className);
+  return el;
+};
+
+
+/**
+ * Returns the event handler for this component, lazily created the first time
+ * this method is called.
+ * @return {!goog.events.EventHandler<T>} Event handler for this component.
+ * @protected
+ * @this {T}
+ * @template T
+ */
+goog.ui.Component.prototype.getHandler = function() {
+  // TODO(17988911): templated "this" values currently result in "this" being
+  // "unknown" in the body of the function.
+  var self = /** @type {goog.ui.Component} */ (this);
+  if (!self.googUiComponentHandler_) {
+    self.googUiComponentHandler_ = new goog.events.EventHandler(self);
+  }
+  return self.googUiComponentHandler_;
+};
+
+
+/**
+ * Sets the parent of this component to use for event bubbling.  Throws an error
+ * if the component already has a parent or if an attempt is made to add a
+ * component to itself as a child.  Callers must use {@code removeChild}
+ * or {@code removeChildAt} to remove components from their containers before
+ * calling this method.
+ * @see goog.ui.Component#removeChild
+ * @see goog.ui.Component#removeChildAt
+ * @param {goog.ui.Component} parent The parent component.
+ */
+goog.ui.Component.prototype.setParent = function(parent) {
+  if (this == parent) {
+    // Attempting to add a child to itself is an error.
+    throw new Error(goog.ui.Component.Error.PARENT_UNABLE_TO_BE_SET);
+  }
+
+  if (parent && this.parent_ && this.id_ && this.parent_.getChild(this.id_) &&
+      this.parent_ != parent) {
+    // This component is already the child of some parent, so it should be
+    // removed using removeChild/removeChildAt first.
+    throw new Error(goog.ui.Component.Error.PARENT_UNABLE_TO_BE_SET);
+  }
+
+  this.parent_ = parent;
+  goog.ui.Component.superClass_.setParentEventTarget.call(this, parent);
+};
+
+
+/**
+ * Returns the component's parent, if any.
+ * @return {goog.ui.Component?} The parent component.
+ */
+goog.ui.Component.prototype.getParent = function() {
+  return this.parent_;
+};
+
+
+/**
+ * Overrides {@link goog.events.EventTarget#setParentEventTarget} to throw an
+ * error if the parent component is set, and the argument is not the parent.
+ * @override
+ */
+goog.ui.Component.prototype.setParentEventTarget = function(parent) {
+  if (this.parent_ && this.parent_ != parent) {
+    throw new Error(goog.ui.Component.Error.NOT_SUPPORTED);
+  }
+  goog.ui.Component.superClass_.setParentEventTarget.call(this, parent);
+};
+
+
+/**
+ * Returns the dom helper that is being used on this component.
+ * @return {!goog.dom.DomHelper} The dom helper used on this component.
+ */
+goog.ui.Component.prototype.getDomHelper = function() {
+  return this.dom_;
+};
+
+
+/**
+ * Determines whether the component has been added to the document.
+ * @return {boolean} TRUE if rendered. Otherwise, FALSE.
+ */
+goog.ui.Component.prototype.isInDocument = function() {
+  return this.inDocument_;
+};
+
+
+/**
+ * Creates the initial DOM representation for the component.  The default
+ * implementation is to set this.element_ = div.
+ */
+goog.ui.Component.prototype.createDom = function() {
+  this.element_ = this.dom_.createElement(goog.dom.TagName.DIV);
+};
+
+
+/**
+ * Renders the component.  If a parent element is supplied, the component's
+ * element will be appended to it.  If there is no optional parent element and
+ * the element doesn't have a parentNode then it will be appended to the
+ * document body.
+ *
+ * If this component has a parent component, and the parent component is
+ * not in the document already, then this will not call {@code enterDocument}
+ * on this component.
+ *
+ * Throws an Error if the component is already rendered.
+ *
+ * @param {Element=} opt_parentElement Optional parent element to render the
+ *    component into.
+ */
+goog.ui.Component.prototype.render = function(opt_parentElement) {
+  this.render_(opt_parentElement);
+};
+
+
+/**
+ * Renders the component before another element. The other element should be in
+ * the document already.
+ *
+ * Throws an Error if the component is already rendered.
+ *
+ * @param {Node} sibling Node to render the component before.
+ */
+goog.ui.Component.prototype.renderBefore = function(sibling) {
+  this.render_(/** @type {Element} */ (sibling.parentNode), sibling);
+};
+
+
+/**
+ * Renders the component.  If a parent element is supplied, the component's
+ * element will be appended to it.  If there is no optional parent element and
+ * the element doesn't have a parentNode then it will be appended to the
+ * document body.
+ *
+ * If this component has a parent component, and the parent component is
+ * not in the document already, then this will not call {@code enterDocument}
+ * on this component.
+ *
+ * Throws an Error if the component is already rendered.
+ *
+ * @param {Element=} opt_parentElement Optional parent element to render the
+ *    component into.
+ * @param {Node=} opt_beforeNode Node before which the component is to
+ *    be rendered.  If left out the node is appended to the parent element.
+ * @private
+ */
+goog.ui.Component.prototype.render_ = function(
+    opt_parentElement, opt_beforeNode) {
+  if (this.inDocument_) {
+    throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);
+  }
+
+  if (!this.element_) {
+    this.createDom();
+  }
+
+  if (opt_parentElement) {
+    opt_parentElement.insertBefore(this.element_, opt_beforeNode || null);
+  } else {
+    this.dom_.getDocument().body.appendChild(this.element_);
+  }
+
+  // If this component has a parent component that isn't in the document yet,
+  // we don't call enterDocument() here.  Instead, when the parent component
+  // enters the document, the enterDocument() call will propagate to its
+  // children, including this one.  If the component doesn't have a parent
+  // or if the parent is already in the document, we call enterDocument().
+  if (!this.parent_ || this.parent_.isInDocument()) {
+    this.enterDocument();
+  }
+};
+
+
+/**
+ * Decorates the element for the UI component. If the element is in the
+ * document, the enterDocument method will be called.
+ *
+ * If goog.ui.Component.ALLOW_DETACHED_DECORATION is false, the caller must
+ * pass an element that is in the document.
+ *
+ * @param {Element} element Element to decorate.
+ */
+goog.ui.Component.prototype.decorate = function(element) {
+  if (this.inDocument_) {
+    throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);
+  } else if (element && this.canDecorate(element)) {
+    this.wasDecorated_ = true;
+
+    // Set the DOM helper of the component to match the decorated element.
+    var doc = goog.dom.getOwnerDocument(element);
+    if (!this.dom_ || this.dom_.getDocument() != doc) {
+      this.dom_ = goog.dom.getDomHelper(element);
+    }
+
+    // Call specific component decorate logic.
+    this.decorateInternal(element);
+
+    // If supporting detached decoration, check that element is in doc.
+    if (!goog.ui.Component.ALLOW_DETACHED_DECORATION ||
+        goog.dom.contains(doc, element)) {
+      this.enterDocument();
+    }
+  } else {
+    throw new Error(goog.ui.Component.Error.DECORATE_INVALID);
+  }
+};
+
+
+/**
+ * Determines if a given element can be decorated by this type of component.
+ * This method should be overridden by inheriting objects.
+ * @param {Element} element Element to decorate.
+ * @return {boolean} True if the element can be decorated, false otherwise.
+ */
+goog.ui.Component.prototype.canDecorate = function(element) {
+  return true;
+};
+
+
+/**
+ * @return {boolean} Whether the component was decorated.
+ */
+goog.ui.Component.prototype.wasDecorated = function() {
+  return this.wasDecorated_;
+};
+
+
+/**
+ * Actually decorates the element. Should be overridden by inheriting objects.
+ * This method can assume there are checks to ensure the component has not
+ * already been rendered have occurred and that enter document will be called
+ * afterwards. This method is considered protected.
+ * @param {Element} element Element to decorate.
+ * @protected
+ */
+goog.ui.Component.prototype.decorateInternal = function(element) {
+  this.element_ = element;
+};
+
+
+/**
+ * Called when the component's element is known to be in the document. Anything
+ * using document.getElementById etc. should be done at this stage.
+ *
+ * If the component contains child components, this call is propagated to its
+ * children.
+ */
+goog.ui.Component.prototype.enterDocument = function() {
+  this.inDocument_ = true;
+
+  // Propagate enterDocument to child components that have a DOM, if any.
+  // If a child was decorated before entering the document (permitted when
+  // goog.ui.Component.ALLOW_DETACHED_DECORATION is true), its enterDocument
+  // will be called here.
+  this.forEachChild(function(child) {
+    if (!child.isInDocument() && child.getElement()) {
+      child.enterDocument();
+    }
+  });
+};
+
+
+/**
+ * Called by dispose to clean up the elements and listeners created by a
+ * component, or by a parent component/application who has removed the
+ * component from the document but wants to reuse it later.
+ *
+ * If the component contains child components, this call is propagated to its
+ * children.
+ *
+ * It should be possible for the component to be rendered again once this method
+ * has been called.
+ */
+goog.ui.Component.prototype.exitDocument = function() {
+  // Propagate exitDocument to child components that have been rendered, if any.
+  this.forEachChild(function(child) {
+    if (child.isInDocument()) {
+      child.exitDocument();
+    }
+  });
+
+  if (this.googUiComponentHandler_) {
+    this.googUiComponentHandler_.removeAll();
+  }
+
+  this.inDocument_ = false;
+};
+
+
+/**
+ * Disposes of the component.  Calls {@code exitDocument}, which is expected to
+ * remove event handlers and clean up the component.  Propagates the call to
+ * the component's children, if any. Removes the component's DOM from the
+ * document unless it was decorated.
+ * @override
+ * @protected
+ */
+goog.ui.Component.prototype.disposeInternal = function() {
+  if (this.inDocument_) {
+    this.exitDocument();
+  }
+
+  if (this.googUiComponentHandler_) {
+    this.googUiComponentHandler_.dispose();
+    delete this.googUiComponentHandler_;
+  }
+
+  // Disposes of the component's children, if any.
+  this.forEachChild(function(child) { child.dispose(); });
+
+  // Detach the component's element from the DOM, unless it was decorated.
+  if (!this.wasDecorated_ && this.element_) {
+    goog.dom.removeNode(this.element_);
+  }
+
+  this.children_ = null;
+  this.childIndex_ = null;
+  this.element_ = null;
+  this.model_ = null;
+  this.parent_ = null;
+
+  goog.ui.Component.superClass_.disposeInternal.call(this);
+};
+
+
+/**
+ * Helper function for subclasses that gets a unique id for a given fragment,
+ * this can be used by components to generate unique string ids for DOM
+ * elements.
+ * @param {string} idFragment A partial id.
+ * @return {string} Unique element id.
+ */
+goog.ui.Component.prototype.makeId = function(idFragment) {
+  return this.getId() + '.' + idFragment;
+};
+
+
+/**
+ * Makes a collection of ids.  This is a convenience method for makeId.  The
+ * object's values are the id fragments and the new values are the generated
+ * ids.  The key will remain the same.
+ * @param {Object} object The object that will be used to create the ids.
+ * @return {!Object<string, string>} An object of id keys to generated ids.
+ */
+goog.ui.Component.prototype.makeIds = function(object) {
+  var ids = {};
+  for (var key in object) {
+    ids[key] = this.makeId(object[key]);
+  }
+  return ids;
+};
+
+
+/**
+ * Returns the model associated with the UI component.
+ * @return {*} The model.
+ */
+goog.ui.Component.prototype.getModel = function() {
+  return this.model_;
+};
+
+
+/**
+ * Sets the model associated with the UI component.
+ * @param {*} obj The model.
+ */
+goog.ui.Component.prototype.setModel = function(obj) {
+  this.model_ = obj;
+};
+
+
+/**
+ * Helper function for returning the fragment portion of an id generated using
+ * makeId().
+ * @param {string} id Id generated with makeId().
+ * @return {string} Fragment.
+ */
+goog.ui.Component.prototype.getFragmentFromId = function(id) {
+  return id.substring(this.getId().length + 1);
+};
+
+
+/**
+ * Helper function for returning an element in the document with a unique id
+ * generated using makeId().
+ * @param {string} idFragment The partial id.
+ * @return {Element} The element with the unique id, or null if it cannot be
+ *     found.
+ */
+goog.ui.Component.prototype.getElementByFragment = function(idFragment) {
+  if (!this.inDocument_) {
+    throw new Error(goog.ui.Component.Error.NOT_IN_DOCUMENT);
+  }
+  return this.dom_.getElement(this.makeId(idFragment));
+};
+
+
+/**
+ * Adds the specified component as the last child of this component.  See
+ * {@link goog.ui.Component#addChildAt} for detailed semantics.
+ *
+ * @see goog.ui.Component#addChildAt
+ * @param {goog.ui.Component} child The new child component.
+ * @param {boolean=} opt_render If true, the child component will be rendered
+ *    into the parent.
+ */
+goog.ui.Component.prototype.addChild = function(child, opt_render) {
+  // TODO(gboyer): addChildAt(child, this.getChildCount(), false) will
+  // reposition any already-rendered child to the end.  Instead, perhaps
+  // addChild(child, false) should never reposition the child; instead, clients
+  // that need the repositioning will use addChildAt explicitly.  Right now,
+  // clients can get around this by calling addChild before calling decorate.
+  this.addChildAt(child, this.getChildCount(), opt_render);
+};
+
+
+/**
+ * Adds the specified component as a child of this component at the given
+ * 0-based index.
+ *
+ * Both {@code addChild} and {@code addChildAt} assume the following contract
+ * between parent and child components:
+ *  <ul>
+ *    <li>the child component's element must be a descendant of the parent
+ *        component's element, and
+ *    <li>the DOM state of the child component must be consistent with the DOM
+ *        state of the parent component (see {@code isInDocument}) in the
+ *        steady state -- the exception is to addChildAt(child, i, false) and
+ *        then immediately decorate/render the child.
+ *  </ul>
+ *
+ * In particular, {@code parent.addChild(child)} will throw an error if the
+ * child component is already in the document, but the parent isn't.
+ *
+ * Clients of this API may call {@code addChild} and {@code addChildAt} with
+ * {@code opt_render} set to true.  If {@code opt_render} is true, calling these
+ * methods will automatically render the child component's element into the
+ * parent component's element. If the parent does not yet have an element, then
+ * {@code createDom} will automatically be invoked on the parent before
+ * rendering the child.
+ *
+ * Invoking {@code parent.addChild(child, true)} will throw an error if the
+ * child component is already in the document, regardless of the parent's DOM
+ * state.
+ *
+ * If {@code opt_render} is true and the parent component is not already
+ * in the document, {@code enterDocument} will not be called on this component
+ * at this point.
+ *
+ * Finally, this method also throws an error if the new child already has a
+ * different parent, or the given index is out of bounds.
+ *
+ * @see goog.ui.Component#addChild
+ * @param {goog.ui.Component} child The new child component.
+ * @param {number} index 0-based index at which the new child component is to be
+ *    added; must be between 0 and the current child count (inclusive).
+ * @param {boolean=} opt_render If true, the child component will be rendered
+ *    into the parent.
+ * @return {void} Nada.
+ */
+goog.ui.Component.prototype.addChildAt = function(child, index, opt_render) {
+  goog.asserts.assert(!!child, 'Provided element must not be null.');
+
+  if (child.inDocument_ && (opt_render || !this.inDocument_)) {
+    // Adding a child that's already in the document is an error, except if the
+    // parent is also in the document and opt_render is false (e.g. decorate()).
+    throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);
+  }
+
+  if (index < 0 || index > this.getChildCount()) {
+    // Allowing sparse child arrays would lead to strange behavior, so we don't.
+    throw new Error(goog.ui.Component.Error.CHILD_INDEX_OUT_OF_BOUNDS);
+  }
+
+  // Create the index and the child array on first use.
+  if (!this.childIndex_ || !this.children_) {
+    this.childIndex_ = {};
+    this.children_ = [];
+  }
+
+  // Moving child within component, remove old reference.
+  if (child.getParent() == this) {
+    goog.object.set(this.childIndex_, child.getId(), child);
+    goog.array.remove(this.children_, child);
+
+    // Add the child to this component.  goog.object.add() throws an error if
+    // a child with the same ID already exists.
+  } else {
+    goog.object.add(this.childIndex_, child.getId(), child);
+  }
+
+  // Set the parent of the child to this component.  This throws an error if
+  // the child is already contained by another component.
+  child.setParent(this);
+  goog.array.insertAt(this.children_, child, index);
+
+  if (child.inDocument_ && this.inDocument_ && child.getParent() == this) {
+    // Changing the position of an existing child, move the DOM node (if
+    // necessary).
+    var contentElement = this.getContentElement();
+    var insertBeforeElement = contentElement.childNodes[index] || null;
+    if (insertBeforeElement != child.getElement()) {
+      contentElement.insertBefore(child.getElement(), insertBeforeElement);
+    }
+  } else if (opt_render) {
+    // If this (parent) component doesn't have a DOM yet, call createDom now
+    // to make sure we render the child component's element into the correct
+    // parent element (otherwise render_ with a null first argument would
+    // render the child into the document body, which is almost certainly not
+    // what we want).
+    if (!this.element_) {
+      this.createDom();
+    }
+    // Render the child into the parent at the appropriate location.  Note that
+    // getChildAt(index + 1) returns undefined if inserting at the end.
+    // TODO(attila): We should have a renderer with a renderChildAt API.
+    var sibling = this.getChildAt(index + 1);
+    // render_() calls enterDocument() if the parent is already in the document.
+    child.render_(this.getContentElement(), sibling ? sibling.element_ : null);
+  } else if (
+      this.inDocument_ && !child.inDocument_ && child.element_ &&
+      child.element_.parentNode &&
+      // Under some circumstances, IE8 implicitly creates a Document Fragment
+      // for detached nodes, so ensure the parent is an Element as it should be.
+      child.element_.parentNode.nodeType == goog.dom.NodeType.ELEMENT) {
+    // We don't touch the DOM, but if the parent is in the document, and the
+    // child element is in the document but not marked as such, then we call
+    // enterDocument on the child.
+    // TODO(gboyer): It would be nice to move this condition entirely, but
+    // there's a large risk of breaking existing applications that manually
+    // append the child to the DOM and then call addChild.
+    child.enterDocument();
+  }
+};
+
+
+/**
+ * Returns the DOM element into which child components are to be rendered,
+ * or null if the component itself hasn't been rendered yet.  This default
+ * implementation returns the component's root element.  Subclasses with
+ * complex DOM structures must override this method.
+ * @return {Element} Element to contain child elements (null if none).
+ */
+goog.ui.Component.prototype.getContentElement = function() {
+  return this.element_;
+};
+
+
+/**
+ * Returns true if the component is rendered right-to-left, false otherwise.
+ * The first time this function is invoked, the right-to-left rendering property
+ * is set if it has not been already.
+ * @return {boolean} Whether the control is rendered right-to-left.
+ */
+goog.ui.Component.prototype.isRightToLeft = function() {
+  if (this.rightToLeft_ == null) {
+    this.rightToLeft_ = goog.style.isRightToLeft(
+        this.inDocument_ ? this.element_ : this.dom_.getDocument().body);
+  }
+  return this.rightToLeft_;
+};
+
+
+/**
+ * Set is right-to-left. This function should be used if the component needs
+ * to know the rendering direction during dom creation (i.e. before
+ * {@link #enterDocument} is called and is right-to-left is set).
+ * @param {boolean} rightToLeft Whether the component is rendered
+ *     right-to-left.
+ */
+goog.ui.Component.prototype.setRightToLeft = function(rightToLeft) {
+  if (this.inDocument_) {
+    throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);
+  }
+  this.rightToLeft_ = rightToLeft;
+};
+
+
+/**
+ * Returns true if the component has children.
+ * @return {boolean} True if the component has children.
+ */
+goog.ui.Component.prototype.hasChildren = function() {
+  return !!this.children_ && this.children_.length != 0;
+};
+
+
+/**
+ * Returns the number of children of this component.
+ * @return {number} The number of children.
+ */
+goog.ui.Component.prototype.getChildCount = function() {
+  return this.children_ ? this.children_.length : 0;
+};
+
+
+/**
+ * Returns an array containing the IDs of the children of this component, or an
+ * empty array if the component has no children.
+ * @return {!Array<string>} Child component IDs.
+ */
+goog.ui.Component.prototype.getChildIds = function() {
+  var ids = [];
+
+  // We don't use goog.object.getKeys(this.childIndex_) because we want to
+  // return the IDs in the correct order as determined by this.children_.
+  this.forEachChild(function(child) {
+    // addChild()/addChildAt() guarantee that the child array isn't sparse.
+    ids.push(child.getId());
+  });
+
+  return ids;
+};
+
+
+/**
+ * Returns the child with the given ID, or null if no such child exists.
+ * @param {string} id Child component ID.
+ * @return {goog.ui.Component?} The child with the given ID; null if none.
+ */
+goog.ui.Component.prototype.getChild = function(id) {
+  // Use childIndex_ for O(1) access by ID.
+  return (this.childIndex_ && id) ?
+      /** @type {goog.ui.Component} */ (
+          goog.object.get(this.childIndex_, id)) ||
+          null :
+      null;
+};
+
+
+/**
+ * Returns the child at the given index, or null if the index is out of bounds.
+ * @param {number} index 0-based index.
+ * @return {goog.ui.Component?} The child at the given index; null if none.
+ */
+goog.ui.Component.prototype.getChildAt = function(index) {
+  // Use children_ for access by index.
+  return this.children_ ? this.children_[index] || null : null;
+};
+
+
+/**
+ * Calls the given function on each of this component's children in order.  If
+ * {@code opt_obj} is provided, it will be used as the 'this' object in the
+ * function when called.  The function should take two arguments:  the child
+ * component and its 0-based index.  The return value is ignored.
+ * @param {function(this:T,?,number):?} f The function to call for every
+ * child component; should take 2 arguments (the child and its index).
+ * @param {T=} opt_obj Used as the 'this' object in f when called.
+ * @template T
+ */
+goog.ui.Component.prototype.forEachChild = function(f, opt_obj) {
+  if (this.children_) {
+    goog.array.forEach(this.children_, f, opt_obj);
+  }
+};
+
+
+/**
+ * Returns the 0-based index of the given child component, or -1 if no such
+ * child is found.
+ * @param {goog.ui.Component?} child The child component.
+ * @return {number} 0-based index of the child component; -1 if not found.
+ */
+goog.ui.Component.prototype.indexOfChild = function(child) {
+  return (this.children_ && child) ? goog.array.indexOf(this.children_, child) :
+                                     -1;
+};
+
+
+/**
+ * Removes the given child from this component, and returns it.  Throws an error
+ * if the argument is invalid or if the specified child isn't found in the
+ * parent component.  The argument can either be a string (interpreted as the
+ * ID of the child component to remove) or the child component itself.
+ *
+ * If {@code opt_unrender} is true, calls {@link goog.ui.component#exitDocument}
+ * on the removed child, and subsequently detaches the child's DOM from the
+ * document.  Otherwise it is the caller's responsibility to clean up the child
+ * component's DOM.
+ *
+ * @see goog.ui.Component#removeChildAt
+ * @param {string|goog.ui.Component|null} child The ID of the child to remove,
+ *    or the child component itself.
+ * @param {boolean=} opt_unrender If true, calls {@code exitDocument} on the
+ *    removed child component, and detaches its DOM from the document.
+ * @return {goog.ui.Component} The removed component, if any.
+ */
+goog.ui.Component.prototype.removeChild = function(child, opt_unrender) {
+  if (child) {
+    // Normalize child to be the object and id to be the ID string.  This also
+    // ensures that the child is really ours.
+    var id = goog.isString(child) ? child : child.getId();
+    child = this.getChild(id);
+
+    if (id && child) {
+      goog.object.remove(this.childIndex_, id);
+      goog.array.remove(this.children_, child);
+
+      if (opt_unrender) {
+        // Remove the child component's DOM from the document.  We have to call
+        // exitDocument first (see documentation).
+        child.exitDocument();
+        if (child.element_) {
+          goog.dom.removeNode(child.element_);
+        }
+      }
+
+      // Child's parent must be set to null after exitDocument is called
+      // so that the child can unlisten to its parent if required.
+      child.setParent(null);
+    }
+  }
+
+  if (!child) {
+    throw new Error(goog.ui.Component.Error.NOT_OUR_CHILD);
+  }
+
+  return /** @type {!goog.ui.Component} */ (child);
+};
+
+
+/**
+ * Removes the child at the given index from this component, and returns it.
+ * Throws an error if the argument is out of bounds, or if the specified child
+ * isn't found in the parent.  See {@link goog.ui.Component#removeChild} for
+ * detailed semantics.
+ *
+ * @see goog.ui.Component#removeChild
+ * @param {number} index 0-based index of the child to remove.
+ * @param {boolean=} opt_unrender If true, calls {@code exitDocument} on the
+ *    removed child component, and detaches its DOM from the document.
+ * @return {goog.ui.Component} The removed component, if any.
+ */
+goog.ui.Component.prototype.removeChildAt = function(index, opt_unrender) {
+  // removeChild(null) will throw error.
+  return this.removeChild(this.getChildAt(index), opt_unrender);
+};
+
+
+/**
+ * Removes every child component attached to this one and returns them.
+ *
+ * @see goog.ui.Component#removeChild
+ * @param {boolean=} opt_unrender If true, calls {@link #exitDocument} on the
+ *    removed child components, and detaches their DOM from the document.
+ * @return {!Array<goog.ui.Component>} The removed components if any.
+ */
+goog.ui.Component.prototype.removeChildren = function(opt_unrender) {
+  var removedChildren = [];
+  while (this.hasChildren()) {
+    removedChildren.push(this.removeChildAt(0, opt_unrender));
+  }
+  return removedChildren;
+};
diff --git a/third_party/ink/closure/ui/idgenerator.js b/third_party/ink/closure/ui/idgenerator.js
new file mode 100644
index 0000000..799dc2b
--- /dev/null
+++ b/third_party/ink/closure/ui/idgenerator.js
@@ -0,0 +1,48 @@
+// Copyright 2008 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 Generator for unique element IDs.
+ *
+ * @author jonp@google.com (Jon Perlow)
+ */
+
+goog.provide('goog.ui.IdGenerator');
+
+
+
+/**
+ * Creates a new id generator.
+ * @constructor
+ * @final
+ */
+goog.ui.IdGenerator = function() {};
+goog.addSingletonGetter(goog.ui.IdGenerator);
+
+
+/**
+ * Next unique ID to use
+ * @type {number}
+ * @private
+ */
+goog.ui.IdGenerator.prototype.nextId_ = 0;
+
+
+/**
+ * Gets the next unique ID.
+ * @return {string} The next unique identifier.
+ */
+goog.ui.IdGenerator.prototype.getNextUniqueId = function() {
+  return ':' + (this.nextId_++).toString(36);
+};
diff --git a/third_party/ink/closure/uri/uri.js b/third_party/ink/closure/uri/uri.js
new file mode 100644
index 0000000..33bb5ac
--- /dev/null
+++ b/third_party/ink/closure/uri/uri.js
@@ -0,0 +1,1550 @@
+// Copyright 2006 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 Class for parsing and formatting URIs.
+ *
+ * Use goog.Uri(string) to parse a URI string.  Use goog.Uri.create(...) to
+ * create a new instance of the goog.Uri object from Uri parts.
+ *
+ * e.g: <code>var myUri = new goog.Uri(window.location);</code>
+ *
+ * Implements RFC 3986 for parsing/formatting URIs.
+ * http://www.ietf.org/rfc/rfc3986.txt
+ *
+ * Some changes have been made to the interface (more like .NETs), though the
+ * internal representation is now of un-encoded parts, this will change the
+ * behavior slightly.
+ *
+ * @author msamuel@google.com (Mike Samuel)
+ * @author pupius@google.com (Dan Pupius) - Ported to Closure
+ * @author jonp@google.com (Jon Perlow) - Optimized for IE6
+ * @author micapolos@google.com (Michal Pociecha-Los) - Dot segments removal
+ */
+
+goog.provide('goog.Uri');
+goog.provide('goog.Uri.QueryData');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.string');
+goog.require('goog.structs');
+goog.require('goog.structs.Map');
+goog.require('goog.uri.utils');
+goog.require('goog.uri.utils.ComponentIndex');
+goog.require('goog.uri.utils.StandardQueryParam');
+
+
+
+/**
+ * This class contains setters and getters for the parts of the URI.
+ * The <code>getXyz</code>/<code>setXyz</code> methods return the decoded part
+ * -- so<code>goog.Uri.parse('/foo%20bar').getPath()</code> will return the
+ * decoded path, <code>/foo bar</code>.
+ *
+ * Reserved characters (see RFC 3986 section 2.2) can be present in
+ * their percent-encoded form in scheme, domain, and path URI components and
+ * will not be auto-decoded. For example:
+ * <code>goog.Uri.parse('rel%61tive/path%2fto/resource').getPath()</code> will
+ * return <code>relative/path%2fto/resource</code>.
+ *
+ * The constructor accepts an optional unparsed, raw URI string.  The parser
+ * is relaxed, so special characters that aren't escaped but don't cause
+ * ambiguities will not cause parse failures.
+ *
+ * All setters return <code>this</code> and so may be chained, a la
+ * <code>goog.Uri.parse('/foo').setFragment('part').toString()</code>.
+ *
+ * @param {*=} opt_uri Optional string URI to parse
+ *        (use goog.Uri.create() to create a URI from parts), or if
+ *        a goog.Uri is passed, a clone is created.
+ * @param {boolean=} opt_ignoreCase If true, #getParameterValue will ignore
+ * the case of the parameter name.
+ *
+ * @throws URIError If opt_uri is provided and URI is malformed (that is,
+ *     if decodeURIComponent fails on any of the URI components).
+ * @constructor
+ * @struct
+ */
+goog.Uri = function(opt_uri, opt_ignoreCase) {
+  /**
+   * Scheme such as "http".
+   * @private {string}
+   */
+  this.scheme_ = '';
+
+  /**
+   * User credentials in the form "username:password".
+   * @private {string}
+   */
+  this.userInfo_ = '';
+
+  /**
+   * Domain part, e.g. "www.google.com".
+   * @private {string}
+   */
+  this.domain_ = '';
+
+  /**
+   * Port, e.g. 8080.
+   * @private {?number}
+   */
+  this.port_ = null;
+
+  /**
+   * Path, e.g. "/tests/img.png".
+   * @private {string}
+   */
+  this.path_ = '';
+
+  /**
+   * The fragment without the #.
+   * @private {string}
+   */
+  this.fragment_ = '';
+
+  /**
+   * Whether or not this Uri should be treated as Read Only.
+   * @private {boolean}
+   */
+  this.isReadOnly_ = false;
+
+  /**
+   * Whether or not to ignore case when comparing query params.
+   * @private {boolean}
+   */
+  this.ignoreCase_ = false;
+
+  /**
+   * Object representing query data.
+   * @private {!goog.Uri.QueryData}
+   */
+  this.queryData_;
+
+  // Parse in the uri string
+  var m;
+  if (opt_uri instanceof goog.Uri) {
+    this.ignoreCase_ =
+        goog.isDef(opt_ignoreCase) ? opt_ignoreCase : opt_uri.getIgnoreCase();
+    this.setScheme(opt_uri.getScheme());
+    this.setUserInfo(opt_uri.getUserInfo());
+    this.setDomain(opt_uri.getDomain());
+    this.setPort(opt_uri.getPort());
+    this.setPath(opt_uri.getPath());
+    this.setQueryData(opt_uri.getQueryData().clone());
+    this.setFragment(opt_uri.getFragment());
+  } else if (opt_uri && (m = goog.uri.utils.split(String(opt_uri)))) {
+    this.ignoreCase_ = !!opt_ignoreCase;
+
+    // Set the parts -- decoding as we do so.
+    // COMPATIBILITY NOTE - In IE, unmatched fields may be empty strings,
+    // whereas in other browsers they will be undefined.
+    this.setScheme(m[goog.uri.utils.ComponentIndex.SCHEME] || '', true);
+    this.setUserInfo(m[goog.uri.utils.ComponentIndex.USER_INFO] || '', true);
+    this.setDomain(m[goog.uri.utils.ComponentIndex.DOMAIN] || '', true);
+    this.setPort(m[goog.uri.utils.ComponentIndex.PORT]);
+    this.setPath(m[goog.uri.utils.ComponentIndex.PATH] || '', true);
+    this.setQueryData(m[goog.uri.utils.ComponentIndex.QUERY_DATA] || '', true);
+    this.setFragment(m[goog.uri.utils.ComponentIndex.FRAGMENT] || '', true);
+
+  } else {
+    this.ignoreCase_ = !!opt_ignoreCase;
+    this.queryData_ = new goog.Uri.QueryData(null, null, this.ignoreCase_);
+  }
+};
+
+
+/**
+ * If true, we preserve the type of query parameters set programmatically.
+ *
+ * This means that if you set a parameter to a boolean, and then call
+ * getParameterValue, you will get a boolean back.
+ *
+ * If false, we will coerce parameters to strings, just as they would
+ * appear in real URIs.
+ *
+ * TODO(nicksantos): Remove this once people have time to fix all tests.
+ *
+ * @type {boolean}
+ */
+goog.Uri.preserveParameterTypesCompatibilityFlag = false;
+
+
+/**
+ * Parameter name added to stop caching.
+ * @type {string}
+ */
+goog.Uri.RANDOM_PARAM = goog.uri.utils.StandardQueryParam.RANDOM;
+
+
+/**
+ * @return {string} The string form of the url.
+ * @override
+ */
+goog.Uri.prototype.toString = function() {
+  var out = [];
+
+  var scheme = this.getScheme();
+  if (scheme) {
+    out.push(
+        goog.Uri.encodeSpecialChars_(
+            scheme, goog.Uri.reDisallowedInSchemeOrUserInfo_, true),
+        ':');
+  }
+
+  var domain = this.getDomain();
+  if (domain || scheme == 'file') {
+    out.push('//');
+
+    var userInfo = this.getUserInfo();
+    if (userInfo) {
+      out.push(
+          goog.Uri.encodeSpecialChars_(
+              userInfo, goog.Uri.reDisallowedInSchemeOrUserInfo_, true),
+          '@');
+    }
+
+    out.push(goog.Uri.removeDoubleEncoding_(goog.string.urlEncode(domain)));
+
+    var port = this.getPort();
+    if (port != null) {
+      out.push(':', String(port));
+    }
+  }
+
+  var path = this.getPath();
+  if (path) {
+    if (this.hasDomain() && path.charAt(0) != '/') {
+      out.push('/');
+    }
+    out.push(
+        goog.Uri.encodeSpecialChars_(
+            path, path.charAt(0) == '/' ? goog.Uri.reDisallowedInAbsolutePath_ :
+                                          goog.Uri.reDisallowedInRelativePath_,
+            true));
+  }
+
+  var query = this.getEncodedQuery();
+  if (query) {
+    out.push('?', query);
+  }
+
+  var fragment = this.getFragment();
+  if (fragment) {
+    out.push(
+        '#', goog.Uri.encodeSpecialChars_(
+                 fragment, goog.Uri.reDisallowedInFragment_));
+  }
+  return out.join('');
+};
+
+
+/**
+ * Resolves the given relative URI (a goog.Uri object), using the URI
+ * represented by this instance as the base URI.
+ *
+ * There are several kinds of relative URIs:<br>
+ * 1. foo - replaces the last part of the path, the whole query and fragment<br>
+ * 2. /foo - replaces the the path, the query and fragment<br>
+ * 3. //foo - replaces everything from the domain on.  foo is a domain name<br>
+ * 4. ?foo - replace the query and fragment<br>
+ * 5. #foo - replace the fragment only
+ *
+ * Additionally, if relative URI has a non-empty path, all ".." and "."
+ * segments will be resolved, as described in RFC 3986.
+ *
+ * @param {!goog.Uri} relativeUri The relative URI to resolve.
+ * @return {!goog.Uri} The resolved URI.
+ */
+goog.Uri.prototype.resolve = function(relativeUri) {
+
+  var absoluteUri = this.clone();
+
+  // we satisfy these conditions by looking for the first part of relativeUri
+  // that is not blank and applying defaults to the rest
+
+  var overridden = relativeUri.hasScheme();
+
+  if (overridden) {
+    absoluteUri.setScheme(relativeUri.getScheme());
+  } else {
+    overridden = relativeUri.hasUserInfo();
+  }
+
+  if (overridden) {
+    absoluteUri.setUserInfo(relativeUri.getUserInfo());
+  } else {
+    overridden = relativeUri.hasDomain();
+  }
+
+  if (overridden) {
+    absoluteUri.setDomain(relativeUri.getDomain());
+  } else {
+    overridden = relativeUri.hasPort();
+  }
+
+  var path = relativeUri.getPath();
+  if (overridden) {
+    absoluteUri.setPort(relativeUri.getPort());
+  } else {
+    overridden = relativeUri.hasPath();
+    if (overridden) {
+      // resolve path properly
+      if (path.charAt(0) != '/') {
+        // path is relative
+        if (this.hasDomain() && !this.hasPath()) {
+          // RFC 3986, section 5.2.3, case 1
+          path = '/' + path;
+        } else {
+          // RFC 3986, section 5.2.3, case 2
+          var lastSlashIndex = absoluteUri.getPath().lastIndexOf('/');
+          if (lastSlashIndex != -1) {
+            path = absoluteUri.getPath().substr(0, lastSlashIndex + 1) + path;
+          }
+        }
+      }
+      path = goog.Uri.removeDotSegments(path);
+    }
+  }
+
+  if (overridden) {
+    absoluteUri.setPath(path);
+  } else {
+    overridden = relativeUri.hasQuery();
+  }
+
+  if (overridden) {
+    absoluteUri.setQueryData(relativeUri.getQueryData().clone());
+  } else {
+    overridden = relativeUri.hasFragment();
+  }
+
+  if (overridden) {
+    absoluteUri.setFragment(relativeUri.getFragment());
+  }
+
+  return absoluteUri;
+};
+
+
+/**
+ * Clones the URI instance.
+ * @return {!goog.Uri} New instance of the URI object.
+ */
+goog.Uri.prototype.clone = function() {
+  return new goog.Uri(this);
+};
+
+
+/**
+ * @return {string} The encoded scheme/protocol for the URI.
+ */
+goog.Uri.prototype.getScheme = function() {
+  return this.scheme_;
+};
+
+
+/**
+ * Sets the scheme/protocol.
+ * @throws URIError If opt_decode is true and newScheme is malformed (that is,
+ *     if decodeURIComponent fails).
+ * @param {string} newScheme New scheme value.
+ * @param {boolean=} opt_decode Optional param for whether to decode new value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setScheme = function(newScheme, opt_decode) {
+  this.enforceReadOnly();
+  this.scheme_ =
+      opt_decode ? goog.Uri.decodeOrEmpty_(newScheme, true) : newScheme;
+
+  // remove an : at the end of the scheme so somebody can pass in
+  // window.location.protocol
+  if (this.scheme_) {
+    this.scheme_ = this.scheme_.replace(/:$/, '');
+  }
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether the scheme has been set.
+ */
+goog.Uri.prototype.hasScheme = function() {
+  return !!this.scheme_;
+};
+
+
+/**
+ * @return {string} The decoded user info.
+ */
+goog.Uri.prototype.getUserInfo = function() {
+  return this.userInfo_;
+};
+
+
+/**
+ * Sets the userInfo.
+ * @throws URIError If opt_decode is true and newUserInfo is malformed (that is,
+ *     if decodeURIComponent fails).
+ * @param {string} newUserInfo New userInfo value.
+ * @param {boolean=} opt_decode Optional param for whether to decode new value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setUserInfo = function(newUserInfo, opt_decode) {
+  this.enforceReadOnly();
+  this.userInfo_ =
+      opt_decode ? goog.Uri.decodeOrEmpty_(newUserInfo) : newUserInfo;
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether the user info has been set.
+ */
+goog.Uri.prototype.hasUserInfo = function() {
+  return !!this.userInfo_;
+};
+
+
+/**
+ * @return {string} The decoded domain.
+ */
+goog.Uri.prototype.getDomain = function() {
+  return this.domain_;
+};
+
+
+/**
+ * Sets the domain.
+ * @throws URIError If opt_decode is true and newDomain is malformed (that is,
+ *     if decodeURIComponent fails).
+ * @param {string} newDomain New domain value.
+ * @param {boolean=} opt_decode Optional param for whether to decode new value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setDomain = function(newDomain, opt_decode) {
+  this.enforceReadOnly();
+  this.domain_ =
+      opt_decode ? goog.Uri.decodeOrEmpty_(newDomain, true) : newDomain;
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether the domain has been set.
+ */
+goog.Uri.prototype.hasDomain = function() {
+  return !!this.domain_;
+};
+
+
+/**
+ * @return {?number} The port number.
+ */
+goog.Uri.prototype.getPort = function() {
+  return this.port_;
+};
+
+
+/**
+ * Sets the port number.
+ * @param {*} newPort Port number. Will be explicitly casted to a number.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setPort = function(newPort) {
+  this.enforceReadOnly();
+
+  if (newPort) {
+    newPort = Number(newPort);
+    if (isNaN(newPort) || newPort < 0) {
+      throw new Error('Bad port number ' + newPort);
+    }
+    this.port_ = newPort;
+  } else {
+    this.port_ = null;
+  }
+
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether the port has been set.
+ */
+goog.Uri.prototype.hasPort = function() {
+  return this.port_ != null;
+};
+
+
+/**
+  * @return {string} The decoded path.
+ */
+goog.Uri.prototype.getPath = function() {
+  return this.path_;
+};
+
+
+/**
+ * Sets the path.
+ * @throws URIError If opt_decode is true and newPath is malformed (that is,
+ *     if decodeURIComponent fails).
+ * @param {string} newPath New path value.
+ * @param {boolean=} opt_decode Optional param for whether to decode new value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setPath = function(newPath, opt_decode) {
+  this.enforceReadOnly();
+  this.path_ = opt_decode ? goog.Uri.decodeOrEmpty_(newPath, true) : newPath;
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether the path has been set.
+ */
+goog.Uri.prototype.hasPath = function() {
+  return !!this.path_;
+};
+
+
+/**
+ * @return {boolean} Whether the query string has been set.
+ */
+goog.Uri.prototype.hasQuery = function() {
+  return this.queryData_.toString() !== '';
+};
+
+
+/**
+ * Sets the query data.
+ * @param {goog.Uri.QueryData|string|undefined} queryData QueryData object.
+ * @param {boolean=} opt_decode Optional param for whether to decode new value.
+ *     Applies only if queryData is a string.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setQueryData = function(queryData, opt_decode) {
+  this.enforceReadOnly();
+
+  if (queryData instanceof goog.Uri.QueryData) {
+    this.queryData_ = queryData;
+    this.queryData_.setIgnoreCase(this.ignoreCase_);
+  } else {
+    if (!opt_decode) {
+      // QueryData accepts encoded query string, so encode it if
+      // opt_decode flag is not true.
+      queryData = goog.Uri.encodeSpecialChars_(
+          queryData, goog.Uri.reDisallowedInQuery_);
+    }
+    this.queryData_ = new goog.Uri.QueryData(queryData, null, this.ignoreCase_);
+  }
+
+  return this;
+};
+
+
+/**
+ * Sets the URI query.
+ * @param {string} newQuery New query value.
+ * @param {boolean=} opt_decode Optional param for whether to decode new value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setQuery = function(newQuery, opt_decode) {
+  return this.setQueryData(newQuery, opt_decode);
+};
+
+
+/**
+ * @return {string} The encoded URI query, not including the ?.
+ */
+goog.Uri.prototype.getEncodedQuery = function() {
+  return this.queryData_.toString();
+};
+
+
+/**
+ * @return {string} The decoded URI query, not including the ?.
+ */
+goog.Uri.prototype.getDecodedQuery = function() {
+  return this.queryData_.toDecodedString();
+};
+
+
+/**
+ * Returns the query data.
+ * @return {!goog.Uri.QueryData} QueryData object.
+ */
+goog.Uri.prototype.getQueryData = function() {
+  return this.queryData_;
+};
+
+
+/**
+ * @return {string} The encoded URI query, not including the ?.
+ *
+ * Warning: This method, unlike other getter methods, returns encoded
+ * value, instead of decoded one.
+ */
+goog.Uri.prototype.getQuery = function() {
+  return this.getEncodedQuery();
+};
+
+
+/**
+ * Sets the value of the named query parameters, clearing previous values for
+ * that key.
+ *
+ * @param {string} key The parameter to set.
+ * @param {*} value The new value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setParameterValue = function(key, value) {
+  this.enforceReadOnly();
+  this.queryData_.set(key, value);
+  return this;
+};
+
+
+/**
+ * Sets the values of the named query parameters, clearing previous values for
+ * that key.  Not new values will currently be moved to the end of the query
+ * string.
+ *
+ * So, <code>goog.Uri.parse('foo?a=b&c=d&e=f').setParameterValues('c', ['new'])
+ * </code> yields <tt>foo?a=b&e=f&c=new</tt>.</p>
+ *
+ * @param {string} key The parameter to set.
+ * @param {*} values The new values. If values is a single
+ *     string then it will be treated as the sole value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setParameterValues = function(key, values) {
+  this.enforceReadOnly();
+
+  if (!goog.isArray(values)) {
+    values = [String(values)];
+  }
+
+  this.queryData_.setValues(key, values);
+
+  return this;
+};
+
+
+/**
+ * Returns the value<b>s</b> for a given cgi parameter as a list of decoded
+ * query parameter values.
+ * @param {string} name The parameter to get values for.
+ * @return {!Array<?>} The values for a given cgi parameter as a list of
+ *     decoded query parameter values.
+ */
+goog.Uri.prototype.getParameterValues = function(name) {
+  return this.queryData_.getValues(name);
+};
+
+
+/**
+ * Returns the first value for a given cgi parameter or undefined if the given
+ * parameter name does not appear in the query string.
+ * @param {string} paramName Unescaped parameter name.
+ * @return {string|undefined} The first value for a given cgi parameter or
+ *     undefined if the given parameter name does not appear in the query
+ *     string.
+ */
+goog.Uri.prototype.getParameterValue = function(paramName) {
+  // NOTE(nicksantos): This type-cast is a lie when
+  // preserveParameterTypesCompatibilityFlag is set to true.
+  // But this should only be set to true in tests.
+  return /** @type {string|undefined} */ (this.queryData_.get(paramName));
+};
+
+
+/**
+ * @return {string} The URI fragment, not including the #.
+ */
+goog.Uri.prototype.getFragment = function() {
+  return this.fragment_;
+};
+
+
+/**
+ * Sets the URI fragment.
+ * @throws URIError If opt_decode is true and newFragment is malformed (that is,
+ *     if decodeURIComponent fails).
+ * @param {string} newFragment New fragment value.
+ * @param {boolean=} opt_decode Optional param for whether to decode new value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setFragment = function(newFragment, opt_decode) {
+  this.enforceReadOnly();
+  this.fragment_ =
+      opt_decode ? goog.Uri.decodeOrEmpty_(newFragment) : newFragment;
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether the URI has a fragment set.
+ */
+goog.Uri.prototype.hasFragment = function() {
+  return !!this.fragment_;
+};
+
+
+/**
+ * Returns true if this has the same domain as that of uri2.
+ * @param {!goog.Uri} uri2 The URI object to compare to.
+ * @return {boolean} true if same domain; false otherwise.
+ */
+goog.Uri.prototype.hasSameDomainAs = function(uri2) {
+  return ((!this.hasDomain() && !uri2.hasDomain()) ||
+          this.getDomain() == uri2.getDomain()) &&
+      ((!this.hasPort() && !uri2.hasPort()) ||
+       this.getPort() == uri2.getPort());
+};
+
+
+/**
+ * Adds a random parameter to the Uri.
+ * @return {!goog.Uri} Reference to this Uri object.
+ */
+goog.Uri.prototype.makeUnique = function() {
+  this.enforceReadOnly();
+  this.setParameterValue(goog.Uri.RANDOM_PARAM, goog.string.getRandomString());
+
+  return this;
+};
+
+
+/**
+ * Removes the named query parameter.
+ *
+ * @param {string} key The parameter to remove.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.removeParameter = function(key) {
+  this.enforceReadOnly();
+  this.queryData_.remove(key);
+  return this;
+};
+
+
+/**
+ * Sets whether Uri is read only. If this goog.Uri is read-only,
+ * enforceReadOnly_ will be called at the start of any function that may modify
+ * this Uri.
+ * @param {boolean} isReadOnly whether this goog.Uri should be read only.
+ * @return {!goog.Uri} Reference to this Uri object.
+ */
+goog.Uri.prototype.setReadOnly = function(isReadOnly) {
+  this.isReadOnly_ = isReadOnly;
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether the URI is read only.
+ */
+goog.Uri.prototype.isReadOnly = function() {
+  return this.isReadOnly_;
+};
+
+
+/**
+ * Checks if this Uri has been marked as read only, and if so, throws an error.
+ * This should be called whenever any modifying function is called.
+ */
+goog.Uri.prototype.enforceReadOnly = function() {
+  if (this.isReadOnly_) {
+    throw new Error('Tried to modify a read-only Uri');
+  }
+};
+
+
+/**
+ * Sets whether to ignore case.
+ * NOTE: If there are already key/value pairs in the QueryData, and
+ * ignoreCase_ is set to false, the keys will all be lower-cased.
+ * @param {boolean} ignoreCase whether this goog.Uri should ignore case.
+ * @return {!goog.Uri} Reference to this Uri object.
+ */
+goog.Uri.prototype.setIgnoreCase = function(ignoreCase) {
+  this.ignoreCase_ = ignoreCase;
+  if (this.queryData_) {
+    this.queryData_.setIgnoreCase(ignoreCase);
+  }
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether to ignore case.
+ */
+goog.Uri.prototype.getIgnoreCase = function() {
+  return this.ignoreCase_;
+};
+
+
+//==============================================================================
+// Static members
+//==============================================================================
+
+
+/**
+ * Creates a uri from the string form.  Basically an alias of new goog.Uri().
+ * If a Uri object is passed to parse then it will return a clone of the object.
+ *
+ * @throws URIError If parsing the URI is malformed. The passed URI components
+ *     should all be parseable by decodeURIComponent.
+ * @param {*} uri Raw URI string or instance of Uri
+ *     object.
+ * @param {boolean=} opt_ignoreCase Whether to ignore the case of parameter
+ * names in #getParameterValue.
+ * @return {!goog.Uri} The new URI object.
+ */
+goog.Uri.parse = function(uri, opt_ignoreCase) {
+  return uri instanceof goog.Uri ? uri.clone() :
+                                   new goog.Uri(uri, opt_ignoreCase);
+};
+
+
+/**
+ * Creates a new goog.Uri object from unencoded parts.
+ *
+ * @param {?string=} opt_scheme Scheme/protocol or full URI to parse.
+ * @param {?string=} opt_userInfo username:password.
+ * @param {?string=} opt_domain www.google.com.
+ * @param {?number=} opt_port 9830.
+ * @param {?string=} opt_path /some/path/to/a/file.html.
+ * @param {string|goog.Uri.QueryData=} opt_query a=1&b=2.
+ * @param {?string=} opt_fragment The fragment without the #.
+ * @param {boolean=} opt_ignoreCase Whether to ignore parameter name case in
+ *     #getParameterValue.
+ *
+ * @return {!goog.Uri} The new URI object.
+ */
+goog.Uri.create = function(
+    opt_scheme, opt_userInfo, opt_domain, opt_port, opt_path, opt_query,
+    opt_fragment, opt_ignoreCase) {
+
+  var uri = new goog.Uri(null, opt_ignoreCase);
+
+  // Only set the parts if they are defined and not empty strings.
+  opt_scheme && uri.setScheme(opt_scheme);
+  opt_userInfo && uri.setUserInfo(opt_userInfo);
+  opt_domain && uri.setDomain(opt_domain);
+  opt_port && uri.setPort(opt_port);
+  opt_path && uri.setPath(opt_path);
+  opt_query && uri.setQueryData(opt_query);
+  opt_fragment && uri.setFragment(opt_fragment);
+
+  return uri;
+};
+
+
+/**
+ * Resolves a relative Uri against a base Uri, accepting both strings and
+ * Uri objects.
+ *
+ * @param {*} base Base Uri.
+ * @param {*} rel Relative Uri.
+ * @return {!goog.Uri} Resolved uri.
+ */
+goog.Uri.resolve = function(base, rel) {
+  if (!(base instanceof goog.Uri)) {
+    base = goog.Uri.parse(base);
+  }
+
+  if (!(rel instanceof goog.Uri)) {
+    rel = goog.Uri.parse(rel);
+  }
+
+  return base.resolve(rel);
+};
+
+
+/**
+ * Removes dot segments in given path component, as described in
+ * RFC 3986, section 5.2.4.
+ *
+ * @param {string} path A non-empty path component.
+ * @return {string} Path component with removed dot segments.
+ */
+goog.Uri.removeDotSegments = function(path) {
+  if (path == '..' || path == '.') {
+    return '';
+
+  } else if (
+      !goog.string.contains(path, './') && !goog.string.contains(path, '/.')) {
+    // This optimization detects uris which do not contain dot-segments,
+    // and as a consequence do not require any processing.
+    return path;
+
+  } else {
+    var leadingSlash = goog.string.startsWith(path, '/');
+    var segments = path.split('/');
+    var out = [];
+
+    for (var pos = 0; pos < segments.length;) {
+      var segment = segments[pos++];
+
+      if (segment == '.') {
+        if (leadingSlash && pos == segments.length) {
+          out.push('');
+        }
+      } else if (segment == '..') {
+        if (out.length > 1 || out.length == 1 && out[0] != '') {
+          out.pop();
+        }
+        if (leadingSlash && pos == segments.length) {
+          out.push('');
+        }
+      } else {
+        out.push(segment);
+        leadingSlash = true;
+      }
+    }
+
+    return out.join('/');
+  }
+};
+
+
+/**
+ * Decodes a value or returns the empty string if it isn't defined or empty.
+ * @throws URIError If decodeURIComponent fails to decode val.
+ * @param {string|undefined} val Value to decode.
+ * @param {boolean=} opt_preserveReserved If true, restricted characters will
+ *     not be decoded.
+ * @return {string} Decoded value.
+ * @private
+ */
+goog.Uri.decodeOrEmpty_ = function(val, opt_preserveReserved) {
+  // Don't use UrlDecode() here because val is not a query parameter.
+  if (!val) {
+    return '';
+  }
+
+  // decodeURI has the same output for '%2f' and '%252f'. We double encode %25
+  // so that we can distinguish between the 2 inputs. This is later undone by
+  // removeDoubleEncoding_.
+  return opt_preserveReserved ? decodeURI(val.replace(/%25/g, '%2525')) :
+                                decodeURIComponent(val);
+};
+
+
+/**
+ * If unescapedPart is non null, then escapes any characters in it that aren't
+ * valid characters in a url and also escapes any special characters that
+ * appear in extra.
+ *
+ * @param {*} unescapedPart The string to encode.
+ * @param {RegExp} extra A character set of characters in [\01-\177].
+ * @param {boolean=} opt_removeDoubleEncoding If true, remove double percent
+ *     encoding.
+ * @return {?string} null iff unescapedPart == null.
+ * @private
+ */
+goog.Uri.encodeSpecialChars_ = function(
+    unescapedPart, extra, opt_removeDoubleEncoding) {
+  if (goog.isString(unescapedPart)) {
+    var encoded = encodeURI(unescapedPart).replace(extra, goog.Uri.encodeChar_);
+    if (opt_removeDoubleEncoding) {
+      // encodeURI double-escapes %XX sequences used to represent restricted
+      // characters in some URI components, remove the double escaping here.
+      encoded = goog.Uri.removeDoubleEncoding_(encoded);
+    }
+    return encoded;
+  }
+  return null;
+};
+
+
+/**
+ * Converts a character in [\01-\177] to its unicode character equivalent.
+ * @param {string} ch One character string.
+ * @return {string} Encoded string.
+ * @private
+ */
+goog.Uri.encodeChar_ = function(ch) {
+  var n = ch.charCodeAt(0);
+  return '%' + ((n >> 4) & 0xf).toString(16) + (n & 0xf).toString(16);
+};
+
+
+/**
+ * Removes double percent-encoding from a string.
+ * @param  {string} doubleEncodedString String
+ * @return {string} String with double encoding removed.
+ * @private
+ */
+goog.Uri.removeDoubleEncoding_ = function(doubleEncodedString) {
+  return doubleEncodedString.replace(/%25([0-9a-fA-F]{2})/g, '%$1');
+};
+
+
+/**
+ * Regular expression for characters that are disallowed in the scheme or
+ * userInfo part of the URI.
+ * @type {RegExp}
+ * @private
+ */
+goog.Uri.reDisallowedInSchemeOrUserInfo_ = /[#\/\?@]/g;
+
+
+/**
+ * Regular expression for characters that are disallowed in a relative path.
+ * Colon is included due to RFC 3986 3.3.
+ * @type {RegExp}
+ * @private
+ */
+goog.Uri.reDisallowedInRelativePath_ = /[\#\?:]/g;
+
+
+/**
+ * Regular expression for characters that are disallowed in an absolute path.
+ * @type {RegExp}
+ * @private
+ */
+goog.Uri.reDisallowedInAbsolutePath_ = /[\#\?]/g;
+
+
+/**
+ * Regular expression for characters that are disallowed in the query.
+ * @type {RegExp}
+ * @private
+ */
+goog.Uri.reDisallowedInQuery_ = /[\#\?@]/g;
+
+
+/**
+ * Regular expression for characters that are disallowed in the fragment.
+ * @type {RegExp}
+ * @private
+ */
+goog.Uri.reDisallowedInFragment_ = /#/g;
+
+
+/**
+ * Checks whether two URIs have the same domain.
+ * @param {string} uri1String First URI string.
+ * @param {string} uri2String Second URI string.
+ * @return {boolean} true if the two URIs have the same domain; false otherwise.
+ */
+goog.Uri.haveSameDomain = function(uri1String, uri2String) {
+  // Differs from goog.uri.utils.haveSameDomain, since this ignores scheme.
+  // TODO(gboyer): Have this just call goog.uri.util.haveSameDomain.
+  var pieces1 = goog.uri.utils.split(uri1String);
+  var pieces2 = goog.uri.utils.split(uri2String);
+  return pieces1[goog.uri.utils.ComponentIndex.DOMAIN] ==
+      pieces2[goog.uri.utils.ComponentIndex.DOMAIN] &&
+      pieces1[goog.uri.utils.ComponentIndex.PORT] ==
+      pieces2[goog.uri.utils.ComponentIndex.PORT];
+};
+
+
+
+/**
+ * Class used to represent URI query parameters.  It is essentially a hash of
+ * name-value pairs, though a name can be present more than once.
+ *
+ * Has the same interface as the collections in goog.structs.
+ *
+ * @param {?string=} opt_query Optional encoded query string to parse into
+ *     the object.
+ * @param {goog.Uri=} opt_uri Optional uri object that should have its
+ *     cache invalidated when this object updates. Deprecated -- this
+ *     is no longer required.
+ * @param {boolean=} opt_ignoreCase If true, ignore the case of the parameter
+ *     name in #get.
+ * @constructor
+ * @struct
+ * @final
+ */
+goog.Uri.QueryData = function(opt_query, opt_uri, opt_ignoreCase) {
+  /**
+   * The map containing name/value or name/array-of-values pairs.
+   * May be null if it requires parsing from the query string.
+   *
+   * We need to use a Map because we cannot guarantee that the key names will
+   * not be problematic for IE.
+   *
+   * @private {goog.structs.Map<string, !Array<*>>}
+   */
+  this.keyMap_ = null;
+
+  /**
+   * The number of params, or null if it requires computing.
+   * @private {?number}
+   */
+  this.count_ = null;
+
+  /**
+   * Encoded query string, or null if it requires computing from the key map.
+   * @private {?string}
+   */
+  this.encodedQuery_ = opt_query || null;
+
+  /**
+   * If true, ignore the case of the parameter name in #get.
+   * @private {boolean}
+   */
+  this.ignoreCase_ = !!opt_ignoreCase;
+};
+
+
+/**
+ * If the underlying key map is not yet initialized, it parses the
+ * query string and fills the map with parsed data.
+ * @private
+ */
+goog.Uri.QueryData.prototype.ensureKeyMapInitialized_ = function() {
+  if (!this.keyMap_) {
+    this.keyMap_ = new goog.structs.Map();
+    this.count_ = 0;
+    if (this.encodedQuery_) {
+      var self = this;
+      goog.uri.utils.parseQueryData(this.encodedQuery_, function(name, value) {
+        self.add(goog.string.urlDecode(name), value);
+      });
+    }
+  }
+};
+
+
+/**
+ * Creates a new query data instance from a map of names and values.
+ *
+ * @param {!goog.structs.Map<string, ?>|!Object} map Map of string parameter
+ *     names to parameter value. If parameter value is an array, it is
+ *     treated as if the key maps to each individual value in the
+ *     array.
+ * @param {goog.Uri=} opt_uri URI object that should have its cache
+ *     invalidated when this object updates.
+ * @param {boolean=} opt_ignoreCase If true, ignore the case of the parameter
+ *     name in #get.
+ * @return {!goog.Uri.QueryData} The populated query data instance.
+ */
+goog.Uri.QueryData.createFromMap = function(map, opt_uri, opt_ignoreCase) {
+  var keys = goog.structs.getKeys(map);
+  if (typeof keys == 'undefined') {
+    throw new Error('Keys are undefined');
+  }
+
+  var queryData = new goog.Uri.QueryData(null, null, opt_ignoreCase);
+  var values = goog.structs.getValues(map);
+  for (var i = 0; i < keys.length; i++) {
+    var key = keys[i];
+    var value = values[i];
+    if (!goog.isArray(value)) {
+      queryData.add(key, value);
+    } else {
+      queryData.setValues(key, value);
+    }
+  }
+  return queryData;
+};
+
+
+/**
+ * Creates a new query data instance from parallel arrays of parameter names
+ * and values. Allows for duplicate parameter names. Throws an error if the
+ * lengths of the arrays differ.
+ *
+ * @param {!Array<string>} keys Parameter names.
+ * @param {!Array<?>} values Parameter values.
+ * @param {goog.Uri=} opt_uri URI object that should have its cache
+ *     invalidated when this object updates.
+ * @param {boolean=} opt_ignoreCase If true, ignore the case of the parameter
+ *     name in #get.
+ * @return {!goog.Uri.QueryData} The populated query data instance.
+ */
+goog.Uri.QueryData.createFromKeysValues = function(
+    keys, values, opt_uri, opt_ignoreCase) {
+  if (keys.length != values.length) {
+    throw new Error('Mismatched lengths for keys/values');
+  }
+  var queryData = new goog.Uri.QueryData(null, null, opt_ignoreCase);
+  for (var i = 0; i < keys.length; i++) {
+    queryData.add(keys[i], values[i]);
+  }
+  return queryData;
+};
+
+
+/**
+ * @return {?number} The number of parameters.
+ */
+goog.Uri.QueryData.prototype.getCount = function() {
+  this.ensureKeyMapInitialized_();
+  return this.count_;
+};
+
+
+/**
+ * Adds a key value pair.
+ * @param {string} key Name.
+ * @param {*} value Value.
+ * @return {!goog.Uri.QueryData} Instance of this object.
+ */
+goog.Uri.QueryData.prototype.add = function(key, value) {
+  this.ensureKeyMapInitialized_();
+  this.invalidateCache_();
+
+  key = this.getKeyName_(key);
+  var values = this.keyMap_.get(key);
+  if (!values) {
+    this.keyMap_.set(key, (values = []));
+  }
+  values.push(value);
+  this.count_ = goog.asserts.assertNumber(this.count_) + 1;
+  return this;
+};
+
+
+/**
+ * Removes all the params with the given key.
+ * @param {string} key Name.
+ * @return {boolean} Whether any parameter was removed.
+ */
+goog.Uri.QueryData.prototype.remove = function(key) {
+  this.ensureKeyMapInitialized_();
+
+  key = this.getKeyName_(key);
+  if (this.keyMap_.containsKey(key)) {
+    this.invalidateCache_();
+
+    // Decrement parameter count.
+    this.count_ =
+        goog.asserts.assertNumber(this.count_) - this.keyMap_.get(key).length;
+    return this.keyMap_.remove(key);
+  }
+  return false;
+};
+
+
+/**
+ * Clears the parameters.
+ */
+goog.Uri.QueryData.prototype.clear = function() {
+  this.invalidateCache_();
+  this.keyMap_ = null;
+  this.count_ = 0;
+};
+
+
+/**
+ * @return {boolean} Whether we have any parameters.
+ */
+goog.Uri.QueryData.prototype.isEmpty = function() {
+  this.ensureKeyMapInitialized_();
+  return this.count_ == 0;
+};
+
+
+/**
+ * Whether there is a parameter with the given name
+ * @param {string} key The parameter name to check for.
+ * @return {boolean} Whether there is a parameter with the given name.
+ */
+goog.Uri.QueryData.prototype.containsKey = function(key) {
+  this.ensureKeyMapInitialized_();
+  key = this.getKeyName_(key);
+  return this.keyMap_.containsKey(key);
+};
+
+
+/**
+ * Whether there is a parameter with the given value.
+ * @param {*} value The value to check for.
+ * @return {boolean} Whether there is a parameter with the given value.
+ */
+goog.Uri.QueryData.prototype.containsValue = function(value) {
+  // NOTE(arv): This solution goes through all the params even if it was the
+  // first param. We can get around this by not reusing code or by switching to
+  // iterators.
+  var vals = this.getValues();
+  return goog.array.contains(vals, value);
+};
+
+
+/**
+ * Runs a callback on every key-value pair in the map, including duplicate keys.
+ * This won't maintain original order when duplicate keys are interspersed (like
+ * getKeys() / getValues()).
+ * @param {function(this:SCOPE, ?, string, !goog.Uri.QueryData)} f
+ * @param {SCOPE=} opt_scope The value of "this" inside f.
+ * @template SCOPE
+ */
+goog.Uri.QueryData.prototype.forEach = function(f, opt_scope) {
+  this.ensureKeyMapInitialized_();
+  this.keyMap_.forEach(function(values, key) {
+    goog.array.forEach(values, function(value) {
+      f.call(opt_scope, value, key, this);
+    }, this);
+  }, this);
+};
+
+
+/**
+ * Returns all the keys of the parameters. If a key is used multiple times
+ * it will be included multiple times in the returned array
+ * @return {!Array<string>} All the keys of the parameters.
+ */
+goog.Uri.QueryData.prototype.getKeys = function() {
+  this.ensureKeyMapInitialized_();
+  // We need to get the values to know how many keys to add.
+  var vals = this.keyMap_.getValues();
+  var keys = this.keyMap_.getKeys();
+  var rv = [];
+  for (var i = 0; i < keys.length; i++) {
+    var val = vals[i];
+    for (var j = 0; j < val.length; j++) {
+      rv.push(keys[i]);
+    }
+  }
+  return rv;
+};
+
+
+/**
+ * Returns all the values of the parameters with the given name. If the query
+ * data has no such key this will return an empty array. If no key is given
+ * all values wil be returned.
+ * @param {string=} opt_key The name of the parameter to get the values for.
+ * @return {!Array<?>} All the values of the parameters with the given name.
+ */
+goog.Uri.QueryData.prototype.getValues = function(opt_key) {
+  this.ensureKeyMapInitialized_();
+  var rv = [];
+  if (goog.isString(opt_key)) {
+    if (this.containsKey(opt_key)) {
+      rv = goog.array.concat(rv, this.keyMap_.get(this.getKeyName_(opt_key)));
+    }
+  } else {
+    // Return all values.
+    var values = this.keyMap_.getValues();
+    for (var i = 0; i < values.length; i++) {
+      rv = goog.array.concat(rv, values[i]);
+    }
+  }
+  return rv;
+};
+
+
+/**
+ * Sets a key value pair and removes all other keys with the same value.
+ *
+ * @param {string} key Name.
+ * @param {*} value Value.
+ * @return {!goog.Uri.QueryData} Instance of this object.
+ */
+goog.Uri.QueryData.prototype.set = function(key, value) {
+  this.ensureKeyMapInitialized_();
+  this.invalidateCache_();
+
+  // TODO(chrishenry): This could be better written as
+  // this.remove(key), this.add(key, value), but that would reorder
+  // the key (since the key is first removed and then added at the
+  // end) and we would have to fix unit tests that depend on key
+  // ordering.
+  key = this.getKeyName_(key);
+  if (this.containsKey(key)) {
+    this.count_ =
+        goog.asserts.assertNumber(this.count_) - this.keyMap_.get(key).length;
+  }
+  this.keyMap_.set(key, [value]);
+  this.count_ = goog.asserts.assertNumber(this.count_) + 1;
+  return this;
+};
+
+
+/**
+ * Returns the first value associated with the key. If the query data has no
+ * such key this will return undefined or the optional default.
+ * @param {string} key The name of the parameter to get the value for.
+ * @param {*=} opt_default The default value to return if the query data
+ *     has no such key.
+ * @return {*} The first string value associated with the key, or opt_default
+ *     if there's no value.
+ */
+goog.Uri.QueryData.prototype.get = function(key, opt_default) {
+  var values = key ? this.getValues(key) : [];
+  if (goog.Uri.preserveParameterTypesCompatibilityFlag) {
+    return values.length > 0 ? values[0] : opt_default;
+  } else {
+    return values.length > 0 ? String(values[0]) : opt_default;
+  }
+};
+
+
+/**
+ * Sets the values for a key. If the key already exists, this will
+ * override all of the existing values that correspond to the key.
+ * @param {string} key The key to set values for.
+ * @param {!Array<?>} values The values to set.
+ */
+goog.Uri.QueryData.prototype.setValues = function(key, values) {
+  this.remove(key);
+
+  if (values.length > 0) {
+    this.invalidateCache_();
+    this.keyMap_.set(this.getKeyName_(key), goog.array.clone(values));
+    this.count_ = goog.asserts.assertNumber(this.count_) + values.length;
+  }
+};
+
+
+/**
+ * @return {string} Encoded query string.
+ * @override
+ */
+goog.Uri.QueryData.prototype.toString = function() {
+  if (this.encodedQuery_) {
+    return this.encodedQuery_;
+  }
+
+  if (!this.keyMap_) {
+    return '';
+  }
+
+  var sb = [];
+
+  // In the past, we use this.getKeys() and this.getVals(), but that
+  // generates a lot of allocations as compared to simply iterating
+  // over the keys.
+  var keys = this.keyMap_.getKeys();
+  for (var i = 0; i < keys.length; i++) {
+    var key = keys[i];
+    var encodedKey = goog.string.urlEncode(key);
+    var val = this.getValues(key);
+    for (var j = 0; j < val.length; j++) {
+      var param = encodedKey;
+      // Ensure that null and undefined are encoded into the url as
+      // literal strings.
+      if (val[j] !== '') {
+        param += '=' + goog.string.urlEncode(val[j]);
+      }
+      sb.push(param);
+    }
+  }
+
+  return this.encodedQuery_ = sb.join('&');
+};
+
+
+/**
+ * @throws URIError If URI is malformed (that is, if decodeURIComponent fails on
+ *     any of the URI components).
+ * @return {string} Decoded query string.
+ */
+goog.Uri.QueryData.prototype.toDecodedString = function() {
+  return goog.Uri.decodeOrEmpty_(this.toString());
+};
+
+
+/**
+ * Invalidate the cache.
+ * @private
+ */
+goog.Uri.QueryData.prototype.invalidateCache_ = function() {
+  this.encodedQuery_ = null;
+};
+
+
+/**
+ * Removes all keys that are not in the provided list. (Modifies this object.)
+ * @param {Array<string>} keys The desired keys.
+ * @return {!goog.Uri.QueryData} a reference to this object.
+ */
+goog.Uri.QueryData.prototype.filterKeys = function(keys) {
+  this.ensureKeyMapInitialized_();
+  this.keyMap_.forEach(function(value, key) {
+    if (!goog.array.contains(keys, key)) {
+      this.remove(key);
+    }
+  }, this);
+  return this;
+};
+
+
+/**
+ * Clone the query data instance.
+ * @return {!goog.Uri.QueryData} New instance of the QueryData object.
+ */
+goog.Uri.QueryData.prototype.clone = function() {
+  var rv = new goog.Uri.QueryData();
+  rv.encodedQuery_ = this.encodedQuery_;
+  if (this.keyMap_) {
+    rv.keyMap_ = this.keyMap_.clone();
+    rv.count_ = this.count_;
+  }
+  return rv;
+};
+
+
+/**
+ * Helper function to get the key name from a JavaScript object. Converts
+ * the object to a string, and to lower case if necessary.
+ * @private
+ * @param {*} arg The object to get a key name from.
+ * @return {string} valid key name which can be looked up in #keyMap_.
+ */
+goog.Uri.QueryData.prototype.getKeyName_ = function(arg) {
+  var keyName = String(arg);
+  if (this.ignoreCase_) {
+    keyName = keyName.toLowerCase();
+  }
+  return keyName;
+};
+
+
+/**
+ * Ignore case in parameter names.
+ * NOTE: If there are already key/value pairs in the QueryData, and
+ * ignoreCase_ is set to false, the keys will all be lower-cased.
+ * @param {boolean} ignoreCase whether this goog.Uri should ignore case.
+ */
+goog.Uri.QueryData.prototype.setIgnoreCase = function(ignoreCase) {
+  var resetKeys = ignoreCase && !this.ignoreCase_;
+  if (resetKeys) {
+    this.ensureKeyMapInitialized_();
+    this.invalidateCache_();
+    this.keyMap_.forEach(function(value, key) {
+      var lowerCase = key.toLowerCase();
+      if (key != lowerCase) {
+        this.remove(key);
+        this.setValues(lowerCase, value);
+      }
+    }, this);
+  }
+  this.ignoreCase_ = ignoreCase;
+};
+
+
+/**
+ * Extends a query data object with another query data or map like object. This
+ * operates 'in-place', it does not create a new QueryData object.
+ *
+ * @param {...(?goog.Uri.QueryData|?goog.structs.Map<?, ?>|?Object)} var_args
+ *     The object from which key value pairs will be copied.
+ * @suppress {deprecated} Use deprecated goog.structs.forEach to allow different
+ * types of parameters.
+ */
+goog.Uri.QueryData.prototype.extend = function(var_args) {
+  for (var i = 0; i < arguments.length; i++) {
+    var data = arguments[i];
+    goog.structs.forEach(
+        data, function(value, key) { this.add(key, value); }, this);
+  }
+};
diff --git a/third_party/ink/closure/uri/utils.js b/third_party/ink/closure/uri/utils.js
new file mode 100644
index 0000000..3b8917ae
--- /dev/null
+++ b/third_party/ink/closure/uri/utils.js
@@ -0,0 +1,1103 @@
+// Copyright 2008 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 Simple utilities for dealing with URI strings.
+ *
+ * This is intended to be a lightweight alternative to constructing goog.Uri
+ * objects.  Whereas goog.Uri adds several kilobytes to the binary regardless
+ * of how much of its functionality you use, this is designed to be a set of
+ * mostly-independent utilities so that the compiler includes only what is
+ * necessary for the task.  Estimated savings of porting is 5k pre-gzip and
+ * 1.5k post-gzip.  To ensure the savings remain, future developers should
+ * avoid adding new functionality to existing functions, but instead create
+ * new ones and factor out shared code.
+ *
+ * Many of these utilities have limited functionality, tailored to common
+ * cases.  The query parameter utilities assume that the parameter keys are
+ * already encoded, since most keys are compile-time alphanumeric strings.  The
+ * query parameter mutation utilities also do not tolerate fragment identifiers.
+ *
+ * By design, these functions can be slower than goog.Uri equivalents.
+ * Repeated calls to some of functions may be quadratic in behavior for IE,
+ * although the effect is somewhat limited given the 2kb limit.
+ *
+ * One advantage of the limited functionality here is that this approach is
+ * less sensitive to differences in URI encodings than goog.Uri, since these
+ * functions operate on strings directly, rather than decoding them and
+ * then re-encoding.
+ *
+ * Uses features of RFC 3986 for parsing/formatting URIs:
+ *   http://www.ietf.org/rfc/rfc3986.txt
+ *
+ * @author gboyer@google.com (Garrett Boyer) - The "lightened" design.
+ * @author msamuel@google.com (Mike Samuel) - Domain knowledge and regexes.
+ */
+
+goog.provide('goog.uri.utils');
+goog.provide('goog.uri.utils.ComponentIndex');
+goog.provide('goog.uri.utils.QueryArray');
+goog.provide('goog.uri.utils.QueryValue');
+goog.provide('goog.uri.utils.StandardQueryParam');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.string');
+
+
+/**
+ * Character codes inlined to avoid object allocations due to charCode.
+ * @enum {number}
+ * @private
+ */
+goog.uri.utils.CharCode_ = {
+  AMPERSAND: 38,
+  EQUAL: 61,
+  HASH: 35,
+  QUESTION: 63
+};
+
+
+/**
+ * Builds a URI string from already-encoded parts.
+ *
+ * No encoding is performed.  Any component may be omitted as either null or
+ * undefined.
+ *
+ * @param {?string=} opt_scheme The scheme such as 'http'.
+ * @param {?string=} opt_userInfo The user name before the '@'.
+ * @param {?string=} opt_domain The domain such as 'www.google.com', already
+ *     URI-encoded.
+ * @param {(string|number|null)=} opt_port The port number.
+ * @param {?string=} opt_path The path, already URI-encoded.  If it is not
+ *     empty, it must begin with a slash.
+ * @param {?string=} opt_queryData The URI-encoded query data.
+ * @param {?string=} opt_fragment The URI-encoded fragment identifier.
+ * @return {string} The fully combined URI.
+ */
+goog.uri.utils.buildFromEncodedParts = function(
+    opt_scheme, opt_userInfo, opt_domain, opt_port, opt_path, opt_queryData,
+    opt_fragment) {
+  var out = '';
+
+  if (opt_scheme) {
+    out += opt_scheme + ':';
+  }
+
+  if (opt_domain) {
+    out += '//';
+
+    if (opt_userInfo) {
+      out += opt_userInfo + '@';
+    }
+
+    out += opt_domain;
+
+    if (opt_port) {
+      out += ':' + opt_port;
+    }
+  }
+
+  if (opt_path) {
+    out += opt_path;
+  }
+
+  if (opt_queryData) {
+    out += '?' + opt_queryData;
+  }
+
+  if (opt_fragment) {
+    out += '#' + opt_fragment;
+  }
+
+  return out;
+};
+
+
+/**
+ * A regular expression for breaking a URI into its component parts.
+ *
+ * {@link http://www.ietf.org/rfc/rfc3986.txt} says in Appendix B
+ * As the "first-match-wins" algorithm is identical to the "greedy"
+ * disambiguation method used by POSIX regular expressions, it is natural and
+ * commonplace to use a regular expression for parsing the potential five
+ * components of a URI reference.
+ *
+ * The following line is the regular expression for breaking-down a
+ * well-formed URI reference into its components.
+ *
+ * <pre>
+ * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+ *  12            3  4          5       6  7        8 9
+ * </pre>
+ *
+ * The numbers in the second line above are only to assist readability; they
+ * indicate the reference points for each subexpression (i.e., each paired
+ * parenthesis). We refer to the value matched for subexpression <n> as $<n>.
+ * For example, matching the above expression to
+ * <pre>
+ *     http://www.ics.uci.edu/pub/ietf/uri/#Related
+ * </pre>
+ * results in the following subexpression matches:
+ * <pre>
+ *    $1 = http:
+ *    $2 = http
+ *    $3 = //www.ics.uci.edu
+ *    $4 = www.ics.uci.edu
+ *    $5 = /pub/ietf/uri/
+ *    $6 = <undefined>
+ *    $7 = <undefined>
+ *    $8 = #Related
+ *    $9 = Related
+ * </pre>
+ * where <undefined> indicates that the component is not present, as is the
+ * case for the query component in the above example. Therefore, we can
+ * determine the value of the five components as
+ * <pre>
+ *    scheme    = $2
+ *    authority = $4
+ *    path      = $5
+ *    query     = $7
+ *    fragment  = $9
+ * </pre>
+ *
+ * The regular expression has been modified slightly to expose the
+ * userInfo, domain, and port separately from the authority.
+ * The modified version yields
+ * <pre>
+ *    $1 = http              scheme
+ *    $2 = <undefined>       userInfo -\
+ *    $3 = www.ics.uci.edu   domain     | authority
+ *    $4 = <undefined>       port     -/
+ *    $5 = /pub/ietf/uri/    path
+ *    $6 = <undefined>       query without ?
+ *    $7 = Related           fragment without #
+ * </pre>
+ * @type {!RegExp}
+ * @private
+ */
+goog.uri.utils.splitRe_ = new RegExp(
+    '^' +
+    '(?:' +
+    '([^:/?#.]+)' +  // scheme - ignore special characters
+                     // used by other URL parts such as :,
+                     // ?, /, #, and .
+    ':)?' +
+    '(?://' +
+    '(?:([^/?#]*)@)?' +  // userInfo
+    '([^/#?]*?)' +       // domain
+    '(?::([0-9]+))?' +   // port
+    '(?=[/#?]|$)' +      // authority-terminating character
+    ')?' +
+    '([^?#]+)?' +          // path
+    '(?:\\?([^#]*))?' +    // query
+    '(?:#([\\s\\S]*))?' +  // fragment
+    '$');
+
+
+/**
+ * The index of each URI component in the return value of goog.uri.utils.split.
+ * @enum {number}
+ */
+goog.uri.utils.ComponentIndex = {
+  SCHEME: 1,
+  USER_INFO: 2,
+  DOMAIN: 3,
+  PORT: 4,
+  PATH: 5,
+  QUERY_DATA: 6,
+  FRAGMENT: 7
+};
+
+
+/**
+ * Splits a URI into its component parts.
+ *
+ * Each component can be accessed via the component indices; for example:
+ * <pre>
+ * goog.uri.utils.split(someStr)[goog.uri.utils.ComponentIndex.QUERY_DATA];
+ * </pre>
+ *
+ * @param {string} uri The URI string to examine.
+ * @return {!Array<string|undefined>} Each component still URI-encoded.
+ *     Each component that is present will contain the encoded value, whereas
+ *     components that are not present will be undefined or empty, depending
+ *     on the browser's regular expression implementation.  Never null, since
+ *     arbitrary strings may still look like path names.
+ */
+goog.uri.utils.split = function(uri) {
+  // See @return comment -- never null.
+  return /** @type {!Array<string|undefined>} */ (
+      uri.match(goog.uri.utils.splitRe_));
+};
+
+
+/**
+ * @param {?string} uri A possibly null string.
+ * @param {boolean=} opt_preserveReserved If true, percent-encoding of RFC-3986
+ *     reserved characters will not be removed.
+ * @return {?string} The string URI-decoded, or null if uri is null.
+ * @private
+ */
+goog.uri.utils.decodeIfPossible_ = function(uri, opt_preserveReserved) {
+  if (!uri) {
+    return uri;
+  }
+
+  return opt_preserveReserved ? decodeURI(uri) : decodeURIComponent(uri);
+};
+
+
+/**
+ * Gets a URI component by index.
+ *
+ * It is preferred to use the getPathEncoded() variety of functions ahead,
+ * since they are more readable.
+ *
+ * @param {goog.uri.utils.ComponentIndex} componentIndex The component index.
+ * @param {string} uri The URI to examine.
+ * @return {?string} The still-encoded component, or null if the component
+ *     is not present.
+ * @private
+ */
+goog.uri.utils.getComponentByIndex_ = function(componentIndex, uri) {
+  // Convert undefined, null, and empty string into null.
+  return goog.uri.utils.split(uri)[componentIndex] || null;
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The protocol or scheme, or null if none.  Does not
+ *     include trailing colons or slashes.
+ */
+goog.uri.utils.getScheme = function(uri) {
+  return goog.uri.utils.getComponentByIndex_(
+      goog.uri.utils.ComponentIndex.SCHEME, uri);
+};
+
+
+/**
+ * Gets the effective scheme for the URL.  If the URL is relative then the
+ * scheme is derived from the page's location.
+ * @param {string} uri The URI to examine.
+ * @return {string} The protocol or scheme, always lower case.
+ */
+goog.uri.utils.getEffectiveScheme = function(uri) {
+  var scheme = goog.uri.utils.getScheme(uri);
+  if (!scheme && goog.global.self && goog.global.self.location) {
+    var protocol = goog.global.self.location.protocol;
+    scheme = protocol.substr(0, protocol.length - 1);
+  }
+  // NOTE: When called from a web worker in Firefox 3.5, location maybe null.
+  // All other browsers with web workers support self.location from the worker.
+  return scheme ? scheme.toLowerCase() : '';
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The user name still encoded, or null if none.
+ */
+goog.uri.utils.getUserInfoEncoded = function(uri) {
+  return goog.uri.utils.getComponentByIndex_(
+      goog.uri.utils.ComponentIndex.USER_INFO, uri);
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The decoded user info, or null if none.
+ */
+goog.uri.utils.getUserInfo = function(uri) {
+  return goog.uri.utils.decodeIfPossible_(
+      goog.uri.utils.getUserInfoEncoded(uri));
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The domain name still encoded, or null if none.
+ */
+goog.uri.utils.getDomainEncoded = function(uri) {
+  return goog.uri.utils.getComponentByIndex_(
+      goog.uri.utils.ComponentIndex.DOMAIN, uri);
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The decoded domain, or null if none.
+ */
+goog.uri.utils.getDomain = function(uri) {
+  return goog.uri.utils.decodeIfPossible_(
+      goog.uri.utils.getDomainEncoded(uri), true /* opt_preserveReserved */);
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?number} The port number, or null if none.
+ */
+goog.uri.utils.getPort = function(uri) {
+  // Coerce to a number.  If the result of getComponentByIndex_ is null or
+  // non-numeric, the number coersion yields NaN.  This will then return
+  // null for all non-numeric cases (though also zero, which isn't a relevant
+  // port number).
+  return Number(
+             goog.uri.utils.getComponentByIndex_(
+                 goog.uri.utils.ComponentIndex.PORT, uri)) ||
+      null;
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The path still encoded, or null if none. Includes the
+ *     leading slash, if any.
+ */
+goog.uri.utils.getPathEncoded = function(uri) {
+  return goog.uri.utils.getComponentByIndex_(
+      goog.uri.utils.ComponentIndex.PATH, uri);
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The decoded path, or null if none.  Includes the leading
+ *     slash, if any.
+ */
+goog.uri.utils.getPath = function(uri) {
+  return goog.uri.utils.decodeIfPossible_(
+      goog.uri.utils.getPathEncoded(uri), true /* opt_preserveReserved */);
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The query data still encoded, or null if none.  Does not
+ *     include the question mark itself.
+ */
+goog.uri.utils.getQueryData = function(uri) {
+  return goog.uri.utils.getComponentByIndex_(
+      goog.uri.utils.ComponentIndex.QUERY_DATA, uri);
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The fragment identifier, or null if none.  Does not
+ *     include the hash mark itself.
+ */
+goog.uri.utils.getFragmentEncoded = function(uri) {
+  // The hash mark may not appear in any other part of the URL.
+  var hashIndex = uri.indexOf('#');
+  return hashIndex < 0 ? null : uri.substr(hashIndex + 1);
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @param {?string} fragment The encoded fragment identifier, or null if none.
+ *     Does not include the hash mark itself.
+ * @return {string} The URI with the fragment set.
+ */
+goog.uri.utils.setFragmentEncoded = function(uri, fragment) {
+  return goog.uri.utils.removeFragment(uri) + (fragment ? '#' + fragment : '');
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The decoded fragment identifier, or null if none.  Does
+ *     not include the hash mark.
+ */
+goog.uri.utils.getFragment = function(uri) {
+  return goog.uri.utils.decodeIfPossible_(
+      goog.uri.utils.getFragmentEncoded(uri));
+};
+
+
+/**
+ * Extracts everything up to the port of the URI.
+ * @param {string} uri The URI string.
+ * @return {string} Everything up to and including the port.
+ */
+goog.uri.utils.getHost = function(uri) {
+  var pieces = goog.uri.utils.split(uri);
+  return goog.uri.utils.buildFromEncodedParts(
+      pieces[goog.uri.utils.ComponentIndex.SCHEME],
+      pieces[goog.uri.utils.ComponentIndex.USER_INFO],
+      pieces[goog.uri.utils.ComponentIndex.DOMAIN],
+      pieces[goog.uri.utils.ComponentIndex.PORT]);
+};
+
+
+/**
+ * Returns the origin for a given URL.
+ * @param {string} uri The URI string.
+ * @return {string} Everything up to and including the port.
+ */
+goog.uri.utils.getOrigin = function(uri) {
+  var pieces = goog.uri.utils.split(uri);
+  return goog.uri.utils.buildFromEncodedParts(
+      pieces[goog.uri.utils.ComponentIndex.SCHEME], null /* opt_userInfo */,
+      pieces[goog.uri.utils.ComponentIndex.DOMAIN],
+      pieces[goog.uri.utils.ComponentIndex.PORT]);
+};
+
+
+/**
+ * Extracts the path of the URL and everything after.
+ * @param {string} uri The URI string.
+ * @return {string} The URI, starting at the path and including the query
+ *     parameters and fragment identifier.
+ */
+goog.uri.utils.getPathAndAfter = function(uri) {
+  var pieces = goog.uri.utils.split(uri);
+  return goog.uri.utils.buildFromEncodedParts(
+      null, null, null, null, pieces[goog.uri.utils.ComponentIndex.PATH],
+      pieces[goog.uri.utils.ComponentIndex.QUERY_DATA],
+      pieces[goog.uri.utils.ComponentIndex.FRAGMENT]);
+};
+
+
+/**
+ * Gets the URI with the fragment identifier removed.
+ * @param {string} uri The URI to examine.
+ * @return {string} Everything preceding the hash mark.
+ */
+goog.uri.utils.removeFragment = function(uri) {
+  // The hash mark may not appear in any other part of the URL.
+  var hashIndex = uri.indexOf('#');
+  return hashIndex < 0 ? uri : uri.substr(0, hashIndex);
+};
+
+
+/**
+ * Ensures that two URI's have the exact same domain, scheme, and port.
+ *
+ * Unlike the version in goog.Uri, this checks protocol, and therefore is
+ * suitable for checking against the browser's same-origin policy.
+ *
+ * @param {string} uri1 The first URI.
+ * @param {string} uri2 The second URI.
+ * @return {boolean} Whether they have the same scheme, domain and port.
+ */
+goog.uri.utils.haveSameDomain = function(uri1, uri2) {
+  var pieces1 = goog.uri.utils.split(uri1);
+  var pieces2 = goog.uri.utils.split(uri2);
+  return pieces1[goog.uri.utils.ComponentIndex.DOMAIN] ==
+      pieces2[goog.uri.utils.ComponentIndex.DOMAIN] &&
+      pieces1[goog.uri.utils.ComponentIndex.SCHEME] ==
+      pieces2[goog.uri.utils.ComponentIndex.SCHEME] &&
+      pieces1[goog.uri.utils.ComponentIndex.PORT] ==
+      pieces2[goog.uri.utils.ComponentIndex.PORT];
+};
+
+
+/**
+ * Asserts that there are no fragment or query identifiers, only in uncompiled
+ * mode.
+ * @param {string} uri The URI to examine.
+ * @private
+ */
+goog.uri.utils.assertNoFragmentsOrQueries_ = function(uri) {
+  goog.asserts.assert(
+      uri.indexOf('#') < 0 && uri.indexOf('?') < 0,
+      'goog.uri.utils: Fragment or query identifiers are not supported: [%s]',
+      uri);
+};
+
+
+/**
+ * Supported query parameter values by the parameter serializing utilities.
+ *
+ * If a value is null or undefined, the key-value pair is skipped, as an easy
+ * way to omit parameters conditionally.  Non-array parameters are converted
+ * to a string and URI encoded.  Array values are expanded into multiple
+ * &key=value pairs, with each element stringized and URI-encoded.
+ *
+ * @typedef {*}
+ */
+goog.uri.utils.QueryValue;
+
+
+/**
+ * An array representing a set of query parameters with alternating keys
+ * and values.
+ *
+ * Keys are assumed to be URI encoded already and live at even indices.  See
+ * goog.uri.utils.QueryValue for details on how parameter values are encoded.
+ *
+ * Example:
+ * <pre>
+ * var data = [
+ *   // Simple param: ?name=BobBarker
+ *   'name', 'BobBarker',
+ *   // Conditional param -- may be omitted entirely.
+ *   'specialDietaryNeeds', hasDietaryNeeds() ? getDietaryNeeds() : null,
+ *   // Multi-valued param: &house=LosAngeles&house=NewYork&house=null
+ *   'house', ['LosAngeles', 'NewYork', null]
+ * ];
+ * </pre>
+ *
+ * @typedef {!Array<string|goog.uri.utils.QueryValue>}
+ */
+goog.uri.utils.QueryArray;
+
+
+/**
+ * Parses encoded query parameters and calls callback function for every
+ * parameter found in the string.
+ *
+ * Missing value of parameter (e.g. “…&key&…”) is treated as if the value was an
+ * empty string.  Keys may be empty strings (e.g. “…&=value&…”) which also means
+ * that “…&=&…” and “…&&…” will result in an empty key and value.
+ *
+ * @param {string} encodedQuery Encoded query string excluding question mark at
+ *     the beginning.
+ * @param {function(string, string)} callback Function called for every
+ *     parameter found in query string.  The first argument (name) will not be
+ *     urldecoded (so the function is consistent with buildQueryData), but the
+ *     second will.  If the parameter has no value (i.e. “=” was not present)
+ *     the second argument (value) will be an empty string.
+ */
+goog.uri.utils.parseQueryData = function(encodedQuery, callback) {
+  if (!encodedQuery) {
+    return;
+  }
+  var pairs = encodedQuery.split('&');
+  for (var i = 0; i < pairs.length; i++) {
+    var indexOfEquals = pairs[i].indexOf('=');
+    var name = null;
+    var value = null;
+    if (indexOfEquals >= 0) {
+      name = pairs[i].substring(0, indexOfEquals);
+      value = pairs[i].substring(indexOfEquals + 1);
+    } else {
+      name = pairs[i];
+    }
+    callback(name, value ? goog.string.urlDecode(value) : '');
+  }
+};
+
+
+/**
+ * Split the URI into 3 parts where the [1] is the queryData without a leading
+ * '?'. For example, the URI http://foo.com/bar?a=b#abc returns
+ * ['http://foo.com/bar','a=b','#abc'].
+ * @param {string} uri The URI to parse.
+ * @return {!Array<string>} An array representation of uri of length 3 where the
+ *     middle value is the queryData without a leading '?'.
+ * @private
+ */
+goog.uri.utils.splitQueryData_ = function(uri) {
+  // Find the query data and and hash.
+  var hashIndex = uri.indexOf('#');
+  if (hashIndex < 0) {
+    hashIndex = uri.length;
+  }
+  var questionIndex = uri.indexOf('?');
+  var queryData;
+  if (questionIndex < 0 || questionIndex > hashIndex) {
+    questionIndex = hashIndex;
+    queryData = '';
+  } else {
+    queryData = uri.substring(questionIndex + 1, hashIndex);
+  }
+  return [uri.substr(0, questionIndex), queryData, uri.substr(hashIndex)];
+};
+
+
+/**
+ * Join an array created by splitQueryData_ back into a URI.
+ * @param {!Array<string>} parts A URI in the form generated by splitQueryData_.
+ * @return {string} The joined URI.
+ * @private
+ */
+goog.uri.utils.joinQueryData_ = function(parts) {
+  return parts[0] + (parts[1] ? '?' + parts[1] : '') + parts[2];
+};
+
+
+/**
+ * @param {string} queryData
+ * @param {string} newData
+ * @return {string}
+ * @private
+ */
+goog.uri.utils.appendQueryData_ = function(queryData, newData) {
+  if (!newData) {
+    return queryData;
+  }
+  return queryData ? queryData + '&' + newData : newData;
+};
+
+
+/**
+ * @param {string} uri
+ * @param {string} queryData
+ * @return {string}
+ * @private
+ */
+goog.uri.utils.appendQueryDataToUri_ = function(uri, queryData) {
+  if (!queryData) {
+    return uri;
+  }
+  var parts = goog.uri.utils.splitQueryData_(uri);
+  parts[1] = goog.uri.utils.appendQueryData_(parts[1], queryData);
+  return goog.uri.utils.joinQueryData_(parts);
+};
+
+
+/**
+ * Appends key=value pairs to an array, supporting multi-valued objects.
+ * @param {*} key The key prefix.
+ * @param {goog.uri.utils.QueryValue} value The value to serialize.
+ * @param {!Array<string>} pairs The array to which the 'key=value' strings
+ *     should be appended.
+ * @private
+ */
+goog.uri.utils.appendKeyValuePairs_ = function(key, value, pairs) {
+  goog.asserts.assertString(key);
+  if (goog.isArray(value)) {
+    // Convince the compiler it's an array.
+    goog.asserts.assertArray(value);
+    for (var j = 0; j < value.length; j++) {
+      // Convert to string explicitly, to short circuit the null and array
+      // logic in this function -- this ensures that null and undefined get
+      // written as literal 'null' and 'undefined', and arrays don't get
+      // expanded out but instead encoded in the default way.
+      goog.uri.utils.appendKeyValuePairs_(key, String(value[j]), pairs);
+    }
+  } else if (value != null) {
+    // Skip a top-level null or undefined entirely.
+    pairs.push(
+        key +
+        // Check for empty string. Zero gets encoded into the url as literal
+        // strings.  For empty string, skip the equal sign, to be consistent
+        // with UriBuilder.java.
+        (value === '' ? '' : '=' + goog.string.urlEncode(value)));
+  }
+};
+
+
+/**
+ * Builds a query data string from a sequence of alternating keys and values.
+ * Currently generates "&key&" for empty args.
+ *
+ * @param {!IArrayLike<string|goog.uri.utils.QueryValue>} keysAndValues
+ *     Alternating keys and values. See the QueryArray typedef.
+ * @param {number=} opt_startIndex A start offset into the arary, defaults to 0.
+ * @return {string} The encoded query string, in the form 'a=1&b=2'.
+ */
+goog.uri.utils.buildQueryData = function(keysAndValues, opt_startIndex) {
+  goog.asserts.assert(
+      Math.max(keysAndValues.length - (opt_startIndex || 0), 0) % 2 == 0,
+      'goog.uri.utils: Key/value lists must be even in length.');
+
+  var params = [];
+  for (var i = opt_startIndex || 0; i < keysAndValues.length; i += 2) {
+    var key = /** @type {string} */ (keysAndValues[i]);
+    goog.uri.utils.appendKeyValuePairs_(key, keysAndValues[i + 1], params);
+  }
+  return params.join('&');
+};
+
+
+/**
+ * Builds a query data string from a map.
+ * Currently generates "&key&" for empty args.
+ *
+ * @param {!Object<string, goog.uri.utils.QueryValue>} map An object where keys
+ *     are URI-encoded parameter keys, and the values are arbitrary types
+ *     or arrays. Keys with a null value are dropped.
+ * @return {string} The encoded query string, in the form 'a=1&b=2'.
+ */
+goog.uri.utils.buildQueryDataFromMap = function(map) {
+  var params = [];
+  for (var key in map) {
+    goog.uri.utils.appendKeyValuePairs_(key, map[key], params);
+  }
+  return params.join('&');
+};
+
+
+/**
+ * Appends URI parameters to an existing URI.
+ *
+ * The variable arguments may contain alternating keys and values.  Keys are
+ * assumed to be already URI encoded.  The values should not be URI-encoded,
+ * and will instead be encoded by this function.
+ * <pre>
+ * appendParams('http://www.foo.com?existing=true',
+ *     'key1', 'value1',
+ *     'key2', 'value?willBeEncoded',
+ *     'key3', ['valueA', 'valueB', 'valueC'],
+ *     'key4', null);
+ * result: 'http://www.foo.com?existing=true&' +
+ *     'key1=value1&' +
+ *     'key2=value%3FwillBeEncoded&' +
+ *     'key3=valueA&key3=valueB&key3=valueC'
+ * </pre>
+ *
+ * A single call to this function will not exhibit quadratic behavior in IE,
+ * whereas multiple repeated calls may, although the effect is limited by
+ * fact that URL's generally can't exceed 2kb.
+ *
+ * @param {string} uri The original URI, which may already have query data.
+ * @param {...(goog.uri.utils.QueryArray|goog.uri.utils.QueryValue)}
+ * var_args
+ *     An array or argument list conforming to goog.uri.utils.QueryArray.
+ * @return {string} The URI with all query parameters added.
+ */
+goog.uri.utils.appendParams = function(uri, var_args) {
+  var queryData = arguments.length == 2 ?
+      goog.uri.utils.buildQueryData(arguments[1], 0) :
+      goog.uri.utils.buildQueryData(arguments, 1);
+  return goog.uri.utils.appendQueryDataToUri_(uri, queryData);
+};
+
+
+/**
+ * Appends query parameters from a map.
+ *
+ * @param {string} uri The original URI, which may already have query data.
+ * @param {!Object<goog.uri.utils.QueryValue>} map An object where keys are
+ *     URI-encoded parameter keys, and the values are arbitrary types or arrays.
+ *     Keys with a null value are dropped.
+ * @return {string} The new parameters.
+ */
+goog.uri.utils.appendParamsFromMap = function(uri, map) {
+  var queryData = goog.uri.utils.buildQueryDataFromMap(map);
+  return goog.uri.utils.appendQueryDataToUri_(uri, queryData);
+};
+
+
+/**
+ * Appends a single URI parameter.
+ *
+ * Repeated calls to this can exhibit quadratic behavior in IE6 due to the
+ * way string append works, though it should be limited given the 2kb limit.
+ *
+ * @param {string} uri The original URI, which may already have query data.
+ * @param {string} key The key, which must already be URI encoded.
+ * @param {*=} opt_value The value, which will be stringized and encoded
+ *     (assumed not already to be encoded).  If omitted, undefined, or null, the
+ *     key will be added as a valueless parameter.
+ * @return {string} The URI with the query parameter added.
+ */
+goog.uri.utils.appendParam = function(uri, key, opt_value) {
+  var value = goog.isDefAndNotNull(opt_value) ?
+      '=' + goog.string.urlEncode(opt_value) :
+      '';
+  return goog.uri.utils.appendQueryDataToUri_(uri, key + value);
+};
+
+
+/**
+ * Finds the next instance of a query parameter with the specified name.
+ *
+ * Does not instantiate any objects.
+ *
+ * @param {string} uri The URI to search.  May contain a fragment identifier
+ *     if opt_hashIndex is specified.
+ * @param {number} startIndex The index to begin searching for the key at.  A
+ *     match may be found even if this is one character after the ampersand.
+ * @param {string} keyEncoded The URI-encoded key.
+ * @param {number} hashOrEndIndex Index to stop looking at.  If a hash
+ *     mark is present, it should be its index, otherwise it should be the
+ *     length of the string.
+ * @return {number} The position of the first character in the key's name,
+ *     immediately after either a question mark or a dot.
+ * @private
+ */
+goog.uri.utils.findParam_ = function(
+    uri, startIndex, keyEncoded, hashOrEndIndex) {
+  var index = startIndex;
+  var keyLength = keyEncoded.length;
+
+  // Search for the key itself and post-filter for surronuding punctuation,
+  // rather than expensively building a regexp.
+  while ((index = uri.indexOf(keyEncoded, index)) >= 0 &&
+         index < hashOrEndIndex) {
+    var precedingChar = uri.charCodeAt(index - 1);
+    // Ensure that the preceding character is '&' or '?'.
+    if (precedingChar == goog.uri.utils.CharCode_.AMPERSAND ||
+        precedingChar == goog.uri.utils.CharCode_.QUESTION) {
+      // Ensure the following character is '&', '=', '#', or NaN
+      // (end of string).
+      var followingChar = uri.charCodeAt(index + keyLength);
+      if (!followingChar || followingChar == goog.uri.utils.CharCode_.EQUAL ||
+          followingChar == goog.uri.utils.CharCode_.AMPERSAND ||
+          followingChar == goog.uri.utils.CharCode_.HASH) {
+        return index;
+      }
+    }
+    index += keyLength + 1;
+  }
+
+  return -1;
+};
+
+
+/**
+ * Regular expression for finding a hash mark or end of string.
+ * @type {RegExp}
+ * @private
+ */
+goog.uri.utils.hashOrEndRe_ = /#|$/;
+
+
+/**
+ * Determines if the URI contains a specific key.
+ *
+ * Performs no object instantiations.
+ *
+ * @param {string} uri The URI to process.  May contain a fragment
+ *     identifier.
+ * @param {string} keyEncoded The URI-encoded key.  Case-sensitive.
+ * @return {boolean} Whether the key is present.
+ */
+goog.uri.utils.hasParam = function(uri, keyEncoded) {
+  return goog.uri.utils.findParam_(
+             uri, 0, keyEncoded, uri.search(goog.uri.utils.hashOrEndRe_)) >= 0;
+};
+
+
+/**
+ * Gets the first value of a query parameter.
+ * @param {string} uri The URI to process.  May contain a fragment.
+ * @param {string} keyEncoded The URI-encoded key.  Case-sensitive.
+ * @return {?string} The first value of the parameter (URI-decoded), or null
+ *     if the parameter is not found.
+ */
+goog.uri.utils.getParamValue = function(uri, keyEncoded) {
+  var hashOrEndIndex = uri.search(goog.uri.utils.hashOrEndRe_);
+  var foundIndex =
+      goog.uri.utils.findParam_(uri, 0, keyEncoded, hashOrEndIndex);
+
+  if (foundIndex < 0) {
+    return null;
+  } else {
+    var endPosition = uri.indexOf('&', foundIndex);
+    if (endPosition < 0 || endPosition > hashOrEndIndex) {
+      endPosition = hashOrEndIndex;
+    }
+    // Progress forth to the end of the "key=" or "key&" substring.
+    foundIndex += keyEncoded.length + 1;
+    // Use substr, because it (unlike substring) will return empty string
+    // if foundIndex > endPosition.
+    return goog.string.urlDecode(
+        uri.substr(foundIndex, endPosition - foundIndex));
+  }
+};
+
+
+/**
+ * Gets all values of a query parameter.
+ * @param {string} uri The URI to process.  May contain a fragment.
+ * @param {string} keyEncoded The URI-encoded key.  Case-sensitive.
+ * @return {!Array<string>} All URI-decoded values with the given key.
+ *     If the key is not found, this will have length 0, but never be null.
+ */
+goog.uri.utils.getParamValues = function(uri, keyEncoded) {
+  var hashOrEndIndex = uri.search(goog.uri.utils.hashOrEndRe_);
+  var position = 0;
+  var foundIndex;
+  var result = [];
+
+  while ((foundIndex = goog.uri.utils.findParam_(
+              uri, position, keyEncoded, hashOrEndIndex)) >= 0) {
+    // Find where this parameter ends, either the '&' or the end of the
+    // query parameters.
+    position = uri.indexOf('&', foundIndex);
+    if (position < 0 || position > hashOrEndIndex) {
+      position = hashOrEndIndex;
+    }
+
+    // Progress forth to the end of the "key=" or "key&" substring.
+    foundIndex += keyEncoded.length + 1;
+    // Use substr, because it (unlike substring) will return empty string
+    // if foundIndex > position.
+    result.push(
+        goog.string.urlDecode(uri.substr(foundIndex, position - foundIndex)));
+  }
+
+  return result;
+};
+
+
+/**
+ * Regexp to find trailing question marks and ampersands.
+ * @type {RegExp}
+ * @private
+ */
+goog.uri.utils.trailingQueryPunctuationRe_ = /[?&]($|#)/;
+
+
+/**
+ * Removes all instances of a query parameter.
+ * @param {string} uri The URI to process.  Must not contain a fragment.
+ * @param {string} keyEncoded The URI-encoded key.
+ * @return {string} The URI with all instances of the parameter removed.
+ */
+goog.uri.utils.removeParam = function(uri, keyEncoded) {
+  var hashOrEndIndex = uri.search(goog.uri.utils.hashOrEndRe_);
+  var position = 0;
+  var foundIndex;
+  var buffer = [];
+
+  // Look for a query parameter.
+  while ((foundIndex = goog.uri.utils.findParam_(
+              uri, position, keyEncoded, hashOrEndIndex)) >= 0) {
+    // Get the portion of the query string up to, but not including, the ?
+    // or & starting the parameter.
+    buffer.push(uri.substring(position, foundIndex));
+    // Progress to immediately after the '&'.  If not found, go to the end.
+    // Avoid including the hash mark.
+    position = Math.min(
+        (uri.indexOf('&', foundIndex) + 1) || hashOrEndIndex, hashOrEndIndex);
+  }
+
+  // Append everything that is remaining.
+  buffer.push(uri.substr(position));
+
+  // Join the buffer, and remove trailing punctuation that remains.
+  return buffer.join('').replace(
+      goog.uri.utils.trailingQueryPunctuationRe_, '$1');
+};
+
+
+/**
+ * Replaces all existing definitions of a parameter with a single definition.
+ *
+ * Repeated calls to this can exhibit quadratic behavior due to the need to
+ * find existing instances and reconstruct the string, though it should be
+ * limited given the 2kb limit.  Consider using appendParams or setParamsFromMap
+ * to update multiple parameters in bulk.
+ *
+ * @param {string} uri The original URI, which may already have query data.
+ * @param {string} keyEncoded The key, which must already be URI encoded.
+ * @param {*} value The value, which will be stringized and encoded (assumed
+ *     not already to be encoded).
+ * @return {string} The URI with the query parameter added.
+ */
+goog.uri.utils.setParam = function(uri, keyEncoded, value) {
+  return goog.uri.utils.appendParam(
+      goog.uri.utils.removeParam(uri, keyEncoded), keyEncoded, value);
+};
+
+
+/**
+ * Effeciently set or remove multiple query parameters in a URI. Order of
+ * unchanged parameters will not be modified, all updated parameters will be
+ * appended to the end of the query. Params with values of null or undefined are
+ * removed.
+ *
+ * @param {string} uri The URI to process.
+ * @param {!Object<string, goog.uri.utils.QueryValue>} params A list of
+ *     parameters to update. If null or undefined, the param will be removed.
+ * @return {string} An updated URI where the query data has been updated with
+ *     the params.
+ */
+goog.uri.utils.setParamsFromMap = function(uri, params) {
+  var parts = goog.uri.utils.splitQueryData_(uri);
+  var queryData = parts[1];
+  var buffer = [];
+  if (queryData) {
+    goog.array.forEach(queryData.split('&'), function(pair) {
+      var indexOfEquals = pair.indexOf('=');
+      var name = indexOfEquals >= 0 ? pair.substr(0, indexOfEquals) : pair;
+      if (!params.hasOwnProperty(name)) {
+        buffer.push(pair);
+      }
+    });
+  }
+  parts[1] = goog.uri.utils.appendQueryData_(
+      buffer.join('&'), goog.uri.utils.buildQueryDataFromMap(params));
+  return goog.uri.utils.joinQueryData_(parts);
+};
+
+
+/**
+ * Generates a URI path using a given URI and a path with checks to
+ * prevent consecutive "//". The baseUri passed in must not contain
+ * query or fragment identifiers. The path to append may not contain query or
+ * fragment identifiers.
+ *
+ * @param {string} baseUri URI to use as the base.
+ * @param {string} path Path to append.
+ * @return {string} Updated URI.
+ */
+goog.uri.utils.appendPath = function(baseUri, path) {
+  goog.uri.utils.assertNoFragmentsOrQueries_(baseUri);
+
+  // Remove any trailing '/'
+  if (goog.string.endsWith(baseUri, '/')) {
+    baseUri = baseUri.substr(0, baseUri.length - 1);
+  }
+  // Remove any leading '/'
+  if (goog.string.startsWith(path, '/')) {
+    path = path.substr(1);
+  }
+  return goog.string.buildString(baseUri, '/', path);
+};
+
+
+/**
+ * Replaces the path.
+ * @param {string} uri URI to use as the base.
+ * @param {string} path New path.
+ * @return {string} Updated URI.
+ */
+goog.uri.utils.setPath = function(uri, path) {
+  // Add any missing '/'.
+  if (!goog.string.startsWith(path, '/')) {
+    path = '/' + path;
+  }
+  var parts = goog.uri.utils.split(uri);
+  return goog.uri.utils.buildFromEncodedParts(
+      parts[goog.uri.utils.ComponentIndex.SCHEME],
+      parts[goog.uri.utils.ComponentIndex.USER_INFO],
+      parts[goog.uri.utils.ComponentIndex.DOMAIN],
+      parts[goog.uri.utils.ComponentIndex.PORT], path,
+      parts[goog.uri.utils.ComponentIndex.QUERY_DATA],
+      parts[goog.uri.utils.ComponentIndex.FRAGMENT]);
+};
+
+
+/**
+ * Standard supported query parameters.
+ * @enum {string}
+ */
+goog.uri.utils.StandardQueryParam = {
+
+  /** Unused parameter for unique-ifying. */
+  RANDOM: 'zx'
+};
+
+
+/**
+ * Sets the zx parameter of a URI to a random value.
+ * @param {string} uri Any URI.
+ * @return {string} That URI with the "zx" parameter added or replaced to
+ *     contain a random string.
+ */
+goog.uri.utils.makeUnique = function(uri) {
+  return goog.uri.utils.setParam(
+      uri, goog.uri.utils.StandardQueryParam.RANDOM,
+      goog.string.getRandomString());
+};
diff --git a/third_party/ink/closure/useragent/product.js b/third_party/ink/closure/useragent/product.js
new file mode 100644
index 0000000..cc85e63
--- /dev/null
+++ b/third_party/ink/closure/useragent/product.js
@@ -0,0 +1,182 @@
+// Copyright 2008 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 Detects the specific browser and not just the rendering engine.
+ *
+ * @author andybons@google.com (Andrew Bonventre)
+ */
+
+goog.provide('goog.userAgent.product');
+
+goog.require('goog.labs.userAgent.browser');
+goog.require('goog.labs.userAgent.platform');
+goog.require('goog.userAgent');
+
+
+/**
+ * @define {boolean} Whether the code is running on the Firefox web browser.
+ */
+goog.define('goog.userAgent.product.ASSUME_FIREFOX', false);
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the product is an
+ *     iPhone.
+ */
+goog.define('goog.userAgent.product.ASSUME_IPHONE', false);
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the product is an
+ *     iPad.
+ */
+goog.define('goog.userAgent.product.ASSUME_IPAD', false);
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the product is an
+ *     AOSP browser or WebView inside a pre KitKat Android phone or tablet.
+ */
+goog.define('goog.userAgent.product.ASSUME_ANDROID', false);
+
+
+/**
+ * @define {boolean} Whether the code is running on the Chrome web browser on
+ * any platform or AOSP browser or WebView in a KitKat+ Android phone or tablet.
+ */
+goog.define('goog.userAgent.product.ASSUME_CHROME', false);
+
+
+/**
+ * @define {boolean} Whether the code is running on the Safari web browser.
+ */
+goog.define('goog.userAgent.product.ASSUME_SAFARI', false);
+
+
+/**
+ * Whether we know the product type at compile-time.
+ * @type {boolean}
+ * @private
+ */
+goog.userAgent.product.PRODUCT_KNOWN_ = goog.userAgent.ASSUME_IE ||
+    goog.userAgent.ASSUME_EDGE || goog.userAgent.ASSUME_OPERA ||
+    goog.userAgent.product.ASSUME_FIREFOX ||
+    goog.userAgent.product.ASSUME_IPHONE ||
+    goog.userAgent.product.ASSUME_IPAD ||
+    goog.userAgent.product.ASSUME_ANDROID ||
+    goog.userAgent.product.ASSUME_CHROME ||
+    goog.userAgent.product.ASSUME_SAFARI;
+
+
+/**
+ * Whether the code is running on the Opera web browser.
+ * @type {boolean}
+ */
+goog.userAgent.product.OPERA = goog.userAgent.OPERA;
+
+
+/**
+ * Whether the code is running on an IE web browser.
+ * @type {boolean}
+ */
+goog.userAgent.product.IE = goog.userAgent.IE;
+
+
+/**
+ * Whether the code is running on an Edge web browser.
+ * @type {boolean}
+ */
+goog.userAgent.product.EDGE = goog.userAgent.EDGE;
+
+
+/**
+ * Whether the code is running on the Firefox web browser.
+ * @type {boolean}
+ */
+goog.userAgent.product.FIREFOX = goog.userAgent.product.PRODUCT_KNOWN_ ?
+    goog.userAgent.product.ASSUME_FIREFOX :
+    goog.labs.userAgent.browser.isFirefox();
+
+
+/**
+ * Whether the user agent is an iPhone or iPod (as in iPod touch).
+ * @return {boolean}
+ * @private
+ */
+goog.userAgent.product.isIphoneOrIpod_ = function() {
+  return goog.labs.userAgent.platform.isIphone() ||
+      goog.labs.userAgent.platform.isIpod();
+};
+
+
+/**
+ * Whether the code is running on an iPhone or iPod touch.
+ *
+ * iPod touch is considered an iPhone for legacy reasons.
+ * @type {boolean}
+ */
+goog.userAgent.product.IPHONE = goog.userAgent.product.PRODUCT_KNOWN_ ?
+    goog.userAgent.product.ASSUME_IPHONE :
+    goog.userAgent.product.isIphoneOrIpod_();
+
+
+/**
+ * Whether the code is running on an iPad.
+ * @type {boolean}
+ */
+goog.userAgent.product.IPAD = goog.userAgent.product.PRODUCT_KNOWN_ ?
+    goog.userAgent.product.ASSUME_IPAD :
+    goog.labs.userAgent.platform.isIpad();
+
+
+/**
+ * Whether the code is running on AOSP browser or WebView inside
+ * a pre KitKat Android phone or tablet.
+ * @type {boolean}
+ */
+goog.userAgent.product.ANDROID = goog.userAgent.product.PRODUCT_KNOWN_ ?
+    goog.userAgent.product.ASSUME_ANDROID :
+    goog.labs.userAgent.browser.isAndroidBrowser();
+
+
+/**
+ * Whether the code is running on the Chrome web browser on any platform
+ * or AOSP browser or WebView in a KitKat+ Android phone or tablet.
+ * @type {boolean}
+ */
+goog.userAgent.product.CHROME = goog.userAgent.product.PRODUCT_KNOWN_ ?
+    goog.userAgent.product.ASSUME_CHROME :
+    goog.labs.userAgent.browser.isChrome();
+
+
+/**
+ * @return {boolean} Whether the browser is Safari on desktop.
+ * @private
+ */
+goog.userAgent.product.isSafariDesktop_ = function() {
+  return goog.labs.userAgent.browser.isSafari() &&
+      !goog.labs.userAgent.platform.isIos();
+};
+
+
+/**
+ * Whether the code is running on the desktop Safari web browser.
+ * Note: the legacy behavior here is only true for Safari not running
+ * on iOS.
+ * @type {boolean}
+ */
+goog.userAgent.product.SAFARI = goog.userAgent.product.PRODUCT_KNOWN_ ?
+    goog.userAgent.product.ASSUME_SAFARI :
+    goog.userAgent.product.isSafariDesktop_();
diff --git a/third_party/ink/closure/useragent/useragent.js b/third_party/ink/closure/useragent/useragent.js
new file mode 100644
index 0000000..007d571
--- /dev/null
+++ b/third_party/ink/closure/useragent/useragent.js
@@ -0,0 +1,581 @@
+// Copyright 2006 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 Rendering engine detection.
+ * @see <a href="http://www.useragentstring.com/">User agent strings</a>
+ * For information on the browser brand (such as Safari versus Chrome), see
+ * goog.userAgent.product.
+ * @author pupius@google.com (Daniel Pupius)
+ * @author arv@google.com (Erik Arvidsson)
+ * @see ../demos/useragent.html
+ */
+
+goog.provide('goog.userAgent');
+
+goog.require('goog.labs.userAgent.browser');
+goog.require('goog.labs.userAgent.engine');
+goog.require('goog.labs.userAgent.platform');
+goog.require('goog.labs.userAgent.util');
+goog.require('goog.reflect');
+goog.require('goog.string');
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the browser is IE.
+ */
+goog.define('goog.userAgent.ASSUME_IE', false);
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the browser is EDGE.
+ */
+goog.define('goog.userAgent.ASSUME_EDGE', false);
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the browser is GECKO.
+ */
+goog.define('goog.userAgent.ASSUME_GECKO', false);
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the browser is WEBKIT.
+ */
+goog.define('goog.userAgent.ASSUME_WEBKIT', false);
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the browser is a
+ *     mobile device running WebKit e.g. iPhone or Android.
+ */
+goog.define('goog.userAgent.ASSUME_MOBILE_WEBKIT', false);
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the browser is OPERA.
+ */
+goog.define('goog.userAgent.ASSUME_OPERA', false);
+
+
+/**
+ * @define {boolean} Whether the
+ *     {@code goog.userAgent.isVersionOrHigher}
+ *     function will return true for any version.
+ */
+goog.define('goog.userAgent.ASSUME_ANY_VERSION', false);
+
+
+/**
+ * Whether we know the browser engine at compile-time.
+ * @type {boolean}
+ * @private
+ */
+goog.userAgent.BROWSER_KNOWN_ = goog.userAgent.ASSUME_IE ||
+    goog.userAgent.ASSUME_EDGE || goog.userAgent.ASSUME_GECKO ||
+    goog.userAgent.ASSUME_MOBILE_WEBKIT || goog.userAgent.ASSUME_WEBKIT ||
+    goog.userAgent.ASSUME_OPERA;
+
+
+/**
+ * Returns the userAgent string for the current browser.
+ *
+ * @return {string} The userAgent string.
+ */
+goog.userAgent.getUserAgentString = function() {
+  return goog.labs.userAgent.util.getUserAgent();
+};
+
+
+/**
+ * TODO(nnaze): Change type to "Navigator" and update compilation targets.
+ * @return {?Object} The native navigator object.
+ */
+goog.userAgent.getNavigator = function() {
+  // Need a local navigator reference instead of using the global one,
+  // to avoid the rare case where they reference different objects.
+  // (in a WorkerPool, for example).
+  return goog.global['navigator'] || null;
+};
+
+
+/**
+ * Whether the user agent is Opera.
+ * @type {boolean}
+ */
+goog.userAgent.OPERA = goog.userAgent.BROWSER_KNOWN_ ?
+    goog.userAgent.ASSUME_OPERA :
+    goog.labs.userAgent.browser.isOpera();
+
+
+/**
+ * Whether the user agent is Internet Explorer.
+ * @type {boolean}
+ */
+goog.userAgent.IE = goog.userAgent.BROWSER_KNOWN_ ?
+    goog.userAgent.ASSUME_IE :
+    goog.labs.userAgent.browser.isIE();
+
+
+/**
+ * Whether the user agent is Microsoft Edge.
+ * @type {boolean}
+ */
+goog.userAgent.EDGE = goog.userAgent.BROWSER_KNOWN_ ?
+    goog.userAgent.ASSUME_EDGE :
+    goog.labs.userAgent.engine.isEdge();
+
+
+/**
+ * Whether the user agent is MS Internet Explorer or MS Edge.
+ * @type {boolean}
+ */
+goog.userAgent.EDGE_OR_IE = goog.userAgent.EDGE || goog.userAgent.IE;
+
+
+/**
+ * Whether the user agent is Gecko. Gecko is the rendering engine used by
+ * Mozilla, Firefox, and others.
+ * @type {boolean}
+ */
+goog.userAgent.GECKO = goog.userAgent.BROWSER_KNOWN_ ?
+    goog.userAgent.ASSUME_GECKO :
+    goog.labs.userAgent.engine.isGecko();
+
+
+/**
+ * Whether the user agent is WebKit. WebKit is the rendering engine that
+ * Safari, Android and others use.
+ * @type {boolean}
+ */
+goog.userAgent.WEBKIT = goog.userAgent.BROWSER_KNOWN_ ?
+    goog.userAgent.ASSUME_WEBKIT || goog.userAgent.ASSUME_MOBILE_WEBKIT :
+    goog.labs.userAgent.engine.isWebKit();
+
+
+/**
+ * Whether the user agent is running on a mobile device.
+ *
+ * This is a separate function so that the logic can be tested.
+ *
+ * TODO(nnaze): Investigate swapping in goog.labs.userAgent.device.isMobile().
+ *
+ * @return {boolean} Whether the user agent is running on a mobile device.
+ * @private
+ */
+goog.userAgent.isMobile_ = function() {
+  return goog.userAgent.WEBKIT &&
+      goog.labs.userAgent.util.matchUserAgent('Mobile');
+};
+
+
+/**
+ * Whether the user agent is running on a mobile device.
+ *
+ * TODO(nnaze): Consider deprecating MOBILE when labs.userAgent
+ *   is promoted as the gecko/webkit logic is likely inaccurate.
+ *
+ * @type {boolean}
+ */
+goog.userAgent.MOBILE =
+    goog.userAgent.ASSUME_MOBILE_WEBKIT || goog.userAgent.isMobile_();
+
+
+/**
+ * Used while transitioning code to use WEBKIT instead.
+ * @type {boolean}
+ * @deprecated Use {@link goog.userAgent.product.SAFARI} instead.
+ * TODO(nicksantos): Delete this from goog.userAgent.
+ */
+goog.userAgent.SAFARI = goog.userAgent.WEBKIT;
+
+
+/**
+ * @return {string} the platform (operating system) the user agent is running
+ *     on. Default to empty string because navigator.platform may not be defined
+ *     (on Rhino, for example).
+ * @private
+ */
+goog.userAgent.determinePlatform_ = function() {
+  var navigator = goog.userAgent.getNavigator();
+  return navigator && navigator.platform || '';
+};
+
+
+/**
+ * The platform (operating system) the user agent is running on. Default to
+ * empty string because navigator.platform may not be defined (on Rhino, for
+ * example).
+ * @type {string}
+ */
+goog.userAgent.PLATFORM = goog.userAgent.determinePlatform_();
+
+
+/**
+ * @define {boolean} Whether the user agent is running on a Macintosh operating
+ *     system.
+ */
+goog.define('goog.userAgent.ASSUME_MAC', false);
+
+
+/**
+ * @define {boolean} Whether the user agent is running on a Windows operating
+ *     system.
+ */
+goog.define('goog.userAgent.ASSUME_WINDOWS', false);
+
+
+/**
+ * @define {boolean} Whether the user agent is running on a Linux operating
+ *     system.
+ */
+goog.define('goog.userAgent.ASSUME_LINUX', false);
+
+
+/**
+ * @define {boolean} Whether the user agent is running on a X11 windowing
+ *     system.
+ */
+goog.define('goog.userAgent.ASSUME_X11', false);
+
+
+/**
+ * @define {boolean} Whether the user agent is running on Android.
+ */
+goog.define('goog.userAgent.ASSUME_ANDROID', false);
+
+
+/**
+ * @define {boolean} Whether the user agent is running on an iPhone.
+ */
+goog.define('goog.userAgent.ASSUME_IPHONE', false);
+
+
+/**
+ * @define {boolean} Whether the user agent is running on an iPad.
+ */
+goog.define('goog.userAgent.ASSUME_IPAD', false);
+
+
+/**
+ * @define {boolean} Whether the user agent is running on an iPod.
+ */
+goog.define('goog.userAgent.ASSUME_IPOD', false);
+
+
+/**
+ * @type {boolean}
+ * @private
+ */
+goog.userAgent.PLATFORM_KNOWN_ = goog.userAgent.ASSUME_MAC ||
+    goog.userAgent.ASSUME_WINDOWS || goog.userAgent.ASSUME_LINUX ||
+    goog.userAgent.ASSUME_X11 || goog.userAgent.ASSUME_ANDROID ||
+    goog.userAgent.ASSUME_IPHONE || goog.userAgent.ASSUME_IPAD ||
+    goog.userAgent.ASSUME_IPOD;
+
+
+/**
+ * Whether the user agent is running on a Macintosh operating system.
+ * @type {boolean}
+ */
+goog.userAgent.MAC = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_MAC :
+    goog.labs.userAgent.platform.isMacintosh();
+
+
+/**
+ * Whether the user agent is running on a Windows operating system.
+ * @type {boolean}
+ */
+goog.userAgent.WINDOWS = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_WINDOWS :
+    goog.labs.userAgent.platform.isWindows();
+
+
+/**
+ * Whether the user agent is Linux per the legacy behavior of
+ * goog.userAgent.LINUX, which considered ChromeOS to also be
+ * Linux.
+ * @return {boolean}
+ * @private
+ */
+goog.userAgent.isLegacyLinux_ = function() {
+  return goog.labs.userAgent.platform.isLinux() ||
+      goog.labs.userAgent.platform.isChromeOS();
+};
+
+
+/**
+ * Whether the user agent is running on a Linux operating system.
+ *
+ * Note that goog.userAgent.LINUX considers ChromeOS to be Linux,
+ * while goog.labs.userAgent.platform considers ChromeOS and
+ * Linux to be different OSes.
+ *
+ * @type {boolean}
+ */
+goog.userAgent.LINUX = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_LINUX :
+    goog.userAgent.isLegacyLinux_();
+
+
+/**
+ * @return {boolean} Whether the user agent is an X11 windowing system.
+ * @private
+ */
+goog.userAgent.isX11_ = function() {
+  var navigator = goog.userAgent.getNavigator();
+  return !!navigator &&
+      goog.string.contains(navigator['appVersion'] || '', 'X11');
+};
+
+
+/**
+ * Whether the user agent is running on a X11 windowing system.
+ * @type {boolean}
+ */
+goog.userAgent.X11 = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_X11 :
+    goog.userAgent.isX11_();
+
+
+/**
+ * Whether the user agent is running on Android.
+ * @type {boolean}
+ */
+goog.userAgent.ANDROID = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_ANDROID :
+    goog.labs.userAgent.platform.isAndroid();
+
+
+/**
+ * Whether the user agent is running on an iPhone.
+ * @type {boolean}
+ */
+goog.userAgent.IPHONE = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_IPHONE :
+    goog.labs.userAgent.platform.isIphone();
+
+
+/**
+ * Whether the user agent is running on an iPad.
+ * @type {boolean}
+ */
+goog.userAgent.IPAD = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_IPAD :
+    goog.labs.userAgent.platform.isIpad();
+
+
+/**
+ * Whether the user agent is running on an iPod.
+ * @type {boolean}
+ */
+goog.userAgent.IPOD = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_IPOD :
+    goog.labs.userAgent.platform.isIpod();
+
+
+/**
+ * Whether the user agent is running on iOS.
+ * @type {boolean}
+ */
+goog.userAgent.IOS = goog.userAgent.PLATFORM_KNOWN_ ?
+    (goog.userAgent.ASSUME_IPHONE || goog.userAgent.ASSUME_IPAD ||
+     goog.userAgent.ASSUME_IPOD) :
+    goog.labs.userAgent.platform.isIos();
+
+/**
+ * @return {string} The string that describes the version number of the user
+ *     agent.
+ * @private
+ */
+goog.userAgent.determineVersion_ = function() {
+  // All browsers have different ways to detect the version and they all have
+  // different naming schemes.
+  // version is a string rather than a number because it may contain 'b', 'a',
+  // and so on.
+  var version = '';
+  var arr = goog.userAgent.getVersionRegexResult_();
+  if (arr) {
+    version = arr ? arr[1] : '';
+  }
+
+  if (goog.userAgent.IE) {
+    // IE9 can be in document mode 9 but be reporting an inconsistent user agent
+    // version.  If it is identifying as a version lower than 9 we take the
+    // documentMode as the version instead.  IE8 has similar behavior.
+    // It is recommended to set the X-UA-Compatible header to ensure that IE9
+    // uses documentMode 9.
+    var docMode = goog.userAgent.getDocumentMode_();
+    if (docMode != null && docMode > parseFloat(version)) {
+      return String(docMode);
+    }
+  }
+
+  return version;
+};
+
+
+/**
+ * @return {?Array|undefined} The version regex matches from parsing the user
+ *     agent string. These regex statements must be executed inline so they can
+ *     be compiled out by the closure compiler with the rest of the useragent
+ *     detection logic when ASSUME_* is specified.
+ * @private
+ */
+goog.userAgent.getVersionRegexResult_ = function() {
+  var userAgent = goog.userAgent.getUserAgentString();
+  if (goog.userAgent.GECKO) {
+    return /rv\:([^\);]+)(\)|;)/.exec(userAgent);
+  }
+  if (goog.userAgent.EDGE) {
+    return /Edge\/([\d\.]+)/.exec(userAgent);
+  }
+  if (goog.userAgent.IE) {
+    return /\b(?:MSIE|rv)[: ]([^\);]+)(\)|;)/.exec(userAgent);
+  }
+  if (goog.userAgent.WEBKIT) {
+    // WebKit/125.4
+    return /WebKit\/(\S+)/.exec(userAgent);
+  }
+  if (goog.userAgent.OPERA) {
+    // If none of the above browsers were detected but the browser is Opera, the
+    // only string that is of interest is 'Version/<number>'.
+    return /(?:Version)[ \/]?(\S+)/.exec(userAgent);
+  }
+  return undefined;
+};
+
+
+/**
+ * @return {number|undefined} Returns the document mode (for testing).
+ * @private
+ */
+goog.userAgent.getDocumentMode_ = function() {
+  // NOTE(pupius): goog.userAgent may be used in context where there is no DOM.
+  var doc = goog.global['document'];
+  return doc ? doc['documentMode'] : undefined;
+};
+
+
+/**
+ * The version of the user agent. This is a string because it might contain
+ * 'b' (as in beta) as well as multiple dots.
+ * @type {string}
+ */
+goog.userAgent.VERSION = goog.userAgent.determineVersion_();
+
+
+/**
+ * Compares two version numbers.
+ *
+ * @param {string} v1 Version of first item.
+ * @param {string} v2 Version of second item.
+ *
+ * @return {number}  1 if first argument is higher
+ *                   0 if arguments are equal
+ *                  -1 if second argument is higher.
+ * @deprecated Use goog.string.compareVersions.
+ */
+goog.userAgent.compare = function(v1, v2) {
+  return goog.string.compareVersions(v1, v2);
+};
+
+
+/**
+ * Cache for {@link goog.userAgent.isVersionOrHigher}.
+ * Calls to compareVersions are surprisingly expensive and, as a browser's
+ * version number is unlikely to change during a session, we cache the results.
+ * @const
+ * @private
+ */
+goog.userAgent.isVersionOrHigherCache_ = {};
+
+
+/**
+ * Whether the user agent version is higher or the same as the given version.
+ * NOTE: When checking the version numbers for Firefox and Safari, be sure to
+ * use the engine's version, not the browser's version number.  For example,
+ * Firefox 3.0 corresponds to Gecko 1.9 and Safari 3.0 to Webkit 522.11.
+ * Opera and Internet Explorer versions match the product release number.<br>
+ * @see <a href="http://en.wikipedia.org/wiki/Safari_version_history">
+ *     Webkit</a>
+ * @see <a href="http://en.wikipedia.org/wiki/Gecko_engine">Gecko</a>
+ *
+ * @param {string|number} version The version to check.
+ * @return {boolean} Whether the user agent version is higher or the same as
+ *     the given version.
+ */
+goog.userAgent.isVersionOrHigher = function(version) {
+  return goog.userAgent.ASSUME_ANY_VERSION ||
+      goog.reflect.cache(
+          goog.userAgent.isVersionOrHigherCache_, version, function() {
+            return goog.string.compareVersions(
+                       goog.userAgent.VERSION, version) >= 0;
+          });
+};
+
+
+/**
+ * Deprecated alias to {@code goog.userAgent.isVersionOrHigher}.
+ * @param {string|number} version The version to check.
+ * @return {boolean} Whether the user agent version is higher or the same as
+ *     the given version.
+ * @deprecated Use goog.userAgent.isVersionOrHigher().
+ */
+goog.userAgent.isVersion = goog.userAgent.isVersionOrHigher;
+
+
+/**
+ * Whether the IE effective document mode is higher or the same as the given
+ * document mode version.
+ * NOTE: Only for IE, return false for another browser.
+ *
+ * @param {number} documentMode The document mode version to check.
+ * @return {boolean} Whether the IE effective document mode is higher or the
+ *     same as the given version.
+ */
+goog.userAgent.isDocumentModeOrHigher = function(documentMode) {
+  return Number(goog.userAgent.DOCUMENT_MODE) >= documentMode;
+};
+
+
+/**
+ * Deprecated alias to {@code goog.userAgent.isDocumentModeOrHigher}.
+ * @param {number} version The version to check.
+ * @return {boolean} Whether the IE effective document mode is higher or the
+ *      same as the given version.
+ * @deprecated Use goog.userAgent.isDocumentModeOrHigher().
+ */
+goog.userAgent.isDocumentMode = goog.userAgent.isDocumentModeOrHigher;
+
+
+/**
+ * For IE version < 7, documentMode is undefined, so attempt to use the
+ * CSS1Compat property to see if we are in standards mode. If we are in
+ * standards mode, treat the browser version as the document mode. Otherwise,
+ * IE is emulating version 5.
+ * @type {number|undefined}
+ * @const
+ */
+goog.userAgent.DOCUMENT_MODE = (function() {
+  var doc = goog.global['document'];
+  var mode = goog.userAgent.getDocumentMode_();
+  if (!doc || !goog.userAgent.IE) {
+    return undefined;
+  }
+  return mode || (doc['compatMode'] == 'CSS1Compat' ?
+                      parseInt(goog.userAgent.VERSION, 10) :
+                      5);
+})();
diff --git a/third_party/ink/ink/web/js/canvas_manager/canvas_manager.js b/third_party/ink/ink/web/js/canvas_manager/canvas_manager.js
new file mode 100644
index 0000000..c4db2c8
--- /dev/null
+++ b/third_party/ink/ink/web/js/canvas_manager/canvas_manager.js
@@ -0,0 +1,598 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+goog.provide('ink.CanvasManager');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.html.SafeUrl');
+goog.require('goog.structs.Set');
+goog.require('goog.ui.Component');
+goog.require('ink.BrushModel');
+goog.require('ink.Color');
+goog.require('ink.ElementListener');
+goog.require('ink.SketchologyEngineWrapper');
+goog.require('ink.embed.events');
+goog.require('ink.util');
+goog.require('sketchology.proto.BackgroundImageInfo');
+goog.require('sketchology.proto.Border');
+goog.require('sketchology.proto.ImageExport');
+goog.require('sketchology.proto.Rect');
+goog.require('sketchology.proto.SetCallbackFlags');
+
+
+
+/**
+ * The controller of the canvas used for drawing.
+ *
+ * @param {?string} engineUrl
+ * @param {ink.util.SEngineType} sengineType
+ * @struct
+ * @constructor
+ * @extends {goog.ui.Component}
+ * @implements {ink.ElementListener}
+ */
+ink.CanvasManager = function(engineUrl, sengineType) {
+  ink.CanvasManager.base(this, 'constructor');
+
+  /** @private {ink.BrushModel} */
+  this.brushModel_ = null;
+
+  /** @private {!ink.SketchologyEngineWrapper} */
+  this.engine_ = new ink.SketchologyEngineWrapper(
+      engineUrl, this, goog.bind(this.onPngExportComplete_, this), sengineType);
+  this.addChild(this.engine_);
+  this.getHandler().listenOnce(
+      this.engine_, ink.SketchologyEngineWrapper.EventType.CANVAS_INITIALIZED,
+      goog.bind(function() {
+        this.brushUpdate_();
+        this.setBorderImage_();
+        this.dispatchEvent(ink.embed.events.EventType.CANVAS_INITIALIZED);
+      }, this));
+
+  // Redispatch CANVAS_FATAL_ERROR events as the top level FATAL_ERROR.
+  this.getHandler().listen(
+      this.engine_, ink.SketchologyEngineWrapper.EventType.CANVAS_FATAL_ERROR,
+      this.dispatchFatalError_);
+
+  this.getHandler().listen(
+      this.engine_, ink.SketchologyEngineWrapper.EventType.PEN_MODE_ENABLED,
+      (ev) => {
+        this.dispatchEvent(new ink.embed.events.PenModeEnabled(ev.enabled));
+      });
+
+  /**
+   * Known element UUIDs from bottom to top
+   * @private {Array.<string>}
+   */
+  this.UUIDs_ = [];
+
+  /**
+   * Set of UUIDs created by the engine but not yet acknowledged by Brix.
+   * @private {goog.structs.Set}}
+   */
+  this.pendingUUIDs_ = new goog.structs.Set();
+
+  /**
+   * Next local ID to use for Brix element bundles missing IDs.
+   * @private {number}
+   */
+  this.nextLocalId_ = 0;
+
+  /**
+   * @const
+   * @type {string}
+   */
+  this.FAKE_UUID = 'fake';
+
+  /**
+   * Background counter
+   * @type {number}
+   * @private
+   */
+  this.bgCount_ = 0;
+
+  /** @private {?function(!goog.html.SafeUrl)} */
+  this.onPngExportCompleteCallback_ = null;
+
+  /** @private {boolean} */
+  this.exportAsBlob_ = false;
+};
+goog.inherits(ink.CanvasManager, goog.ui.Component);
+
+
+/** @const */
+ink.CanvasManager.BORDER_IMAGE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEU' +
+    'gAAAFgAAABYCAYAAABxlTA0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAm' +
+    'pwYAAAAB3RJTUUH4AgPEBYrHoEFUgAAAw1JREFUeNrt3b9uE0EQBvBvZvd8LURUkVwEkEDAm' +
+    '9AgQYOQKHgZWloqUMoUaVJSpAivQBEhJGhSWiAlKXz7JwXey9nE9vrOgHT3fZKVFM5F/mUyN' +
+    '5tIY8F2EtG/yNYucnZ21keg/57d3V1RMvzdEJjABGYITGACM+1iNz5RxGGPzCIbnT+i3RA1A' +
+    'jgFcADgR4zRDwTVABjHGF8AeJwOaDnYNgd29jGGED6JyMvxeDwZYvVOJpN3FxcXH0TkWfMov' +
+    'Qpac6p39jgdMi4A7Ozs/HLOvQkhfEkurW9yDVh47+GcOxgybsre3t5P59y+937OqHUFhxAQQ' +
+    'kBVVd85E/yOc+5bcuk0pqWLeO/hvQ+krV289x45yLqqPSRk5xym0yllZ5lOp6iqCiGEtW3C5' +
+    'lZwzq/DUOK9h4jUwK3GtGYFz1oEZa97cA2crJaNapozoiVk5rqCm+2h898ihn487uKiWPMPy' +
+    '2arYG7G7TQHM91CYAITmCEwgQnMEJjABGYITGCGwAQmMENgAhOYITCBCUwCAhOYITCBCcwQm' +
+    'MAEZghMYIbABCYwQ2ACE5ghMIEJzBCYwAyBCUxghsAEJjBDYAIzBP63wFkLcUX4lhAp3nu79' +
+    'Qr23t8lbZ37WwNuLFx7fnJycnvossfHx7dijK+WGLWrYBFBjPHh5eXl/tHR0Z0h41ZV9T7G+' +
+    'EREum/AThcREaiqqOpTAJ8PDw8/VlX11TnnhgArIrYsy3vn5+evy7J8NLNADrJd1xpUFcaY9' +
+    'BBVfSAib2foAPq7GTAZGGNSkcFaC2stjDH161+BLGsrOAEXRYGyLOsNgKqK5hbovgIng/T6R' +
+    '6MRiqKogVtVcPqi5k+tKIo5XOfcYICNMbDWYjQaoSzLP4BXtYqVFayqiDHCWlsjJnDnXPYG0' +
+    'j5UcCqy9LDWtq/gZcipHxVFMbfitec3uMX70FwP7nyTW2zyaSt236v3pipO7UJVs9pDVgU3p' +
+    '4n0jZqwQwBeHFlzYLPn4MXPb4Lt+5i2CJ1zgsuu4GUX2vBNk3qJnvX8jOdwv3h7O1wBYIqaD' +
+    '5lCtYoAAAAASUVORK5CYII=';
+
+
+/**
+ * This color needs to match BORDER_IMAGE.
+ * @const
+ */
+ink.CanvasManager.OUT_OF_BOUNDS_COLOR = 0xe6e6e6ff;
+
+
+/** @override */
+ink.CanvasManager.prototype.enterDocument = function() {
+  ink.CanvasManager.base(this, 'enterDocument');
+
+  this.engine_.render(this.getElement());
+
+  var handler = this.getHandler();
+  goog.asserts.assert(handler);
+
+  this.brushModel_ = ink.BrushModel.getInstance(this);
+
+  handler.listen(
+      this.brushModel_, ink.BrushModel.EventType.CHANGE, this.brushUpdate_);
+};
+
+/**
+ * Sets or unsets readOnly on the engine.
+ * @param {boolean} readOnly
+ */
+ink.CanvasManager.prototype.setReadOnly = function(readOnly) {
+  this.engine_.setReadOnly(readOnly);
+};
+
+
+/**
+ * Export the scene as a PNG from the engine.
+ * @param {number} maxWidth
+ * @param {boolean} drawBackground
+ * @param {function(!goog.html.SafeUrl)} callback
+ * @param {boolean=} opt_asBlob
+ */
+ink.CanvasManager.prototype.exportPng = function(
+    maxWidth, drawBackground, callback, opt_asBlob) {
+  this.onPngExportCompleteCallback_ = callback;
+  this.exportAsBlob_ = !!opt_asBlob;
+  var exportProto = new sketchology.proto.ImageExport();
+  exportProto.setMaxDimensionPx(maxWidth);
+  exportProto.setShouldDrawBackground(drawBackground);
+  this.engine_.exportPng(exportProto);
+};
+
+
+/**
+ * @private
+ * @param {number} width
+ * @param {number} height
+ * @param {Uint8ClampedArray} bytesArr
+ */
+ink.CanvasManager.prototype.onPngExportComplete_ = function(
+    width, height, bytesArr) {
+  if (this.onPngExportCompleteCallback_) {
+    try {
+      var imageData = new ImageData(bytesArr, width, height);
+      var scratchCanvas =
+          /** @type {!HTMLCanvasElement} */ (document.createElement('canvas'));
+      var scratchContext =
+          /** @type {!CanvasRenderingContext2D} */ (
+              scratchCanvas.getContext('2d'));
+      scratchCanvas.width = width;
+      scratchCanvas.height = height;
+      scratchContext.putImageData(imageData, 0, 0);
+    } catch (ex) {
+      this.dispatchFatalError_(ex);
+      return;
+    }
+    if (this.exportAsBlob_) {
+      var cb = (blob) => {
+        this.onPngExportCompleteCallback_(goog.html.SafeUrl.fromBlob(blob));
+      };
+
+      if (scratchCanvas['msToBlob']) {
+        scratchCanvas['msToBlob'](cb, 'image/png');
+      } else {
+        scratchCanvas.toBlob(cb, 'image/png');
+      }
+    } else {
+      this.onPngExportCompleteCallback_(
+          goog.html.SafeUrl.fromDataUrl(scratchCanvas.toDataURL()));
+    }
+  }
+};
+
+
+/**
+ * Sets a background image, and sets the page bounds to match the image size.
+ * @param {Uint8ClampedArray} data The image data in RGBA 8888.
+ * @param {goog.math.Size} size The image dimensions.
+ */
+ink.CanvasManager.prototype.setBackgroundImage = function(data, size) {
+  this.setBackgroundImage_(data, size);
+};
+
+
+/**
+ * Sets a background image and scales the image to match the existing page
+ * bounds.  Will not display the background if no page bounds are set.
+ * @param {Uint8ClampedArray} data The image data in RGBA 8888.
+ * @param {goog.math.Size} size The image dimensions.
+ */
+ink.CanvasManager.prototype.setImageToUseForPageBackground = function(
+    data, size) {
+  this.setBackgroundImage_(data, size, {'bounds': 'none'});
+};
+
+
+/**
+ * @private
+ * @param {Uint8ClampedArray} data The image data in RGBA 8888.
+ * @param {goog.math.Size} size The image dimensions.
+ * @param {Object<string, *>=} opt_options
+ */
+ink.CanvasManager.prototype.setBackgroundImage_ = function(
+    data, size, opt_options) {
+  opt_options = opt_options || {};
+
+  var nextUri = 'sketchology://background_' + this.bgCount_;
+  this.bgCount_++;
+
+  var bgImageProto = new sketchology.proto.BackgroundImageInfo();
+  bgImageProto.setUri(nextUri);
+  if (opt_options['bounds'] != 'none') {
+    var optBounds = opt_options['bounds'] ||
+        {'xlow': 0, 'ylow': 0, 'xhigh': size.width, 'yhigh': size.height};
+    var bounds = new sketchology.proto.Rect();
+    bounds.setXlow(optBounds['xlow']);
+    bounds.setYlow(optBounds['ylow']);
+    bounds.setXhigh(optBounds['xhigh']);
+    bounds.setYhigh(optBounds['yhigh']);
+    bgImageProto.setBounds(bounds);
+  }
+  this.engine_.setBackgroundImage(data, size, nextUri, bgImageProto);
+};
+
+
+/**
+ * Set background color.
+ * @param {ink.Color} color
+ */
+ink.CanvasManager.prototype.setBackgroundColor = function(color) {
+  this.engine_.setBackgroundColor(color);
+};
+
+
+/** @private */
+ink.CanvasManager.prototype.brushUpdate_ = function() {
+  goog.asserts.assert(this.brushModel_);
+
+  var tool_type = this.brushModel_.getToolType();
+  var brush_type = this.brushModel_.getBrushType();
+  var strokeWidth = this.brushModel_.getStrokeWidth();
+
+  var colorString = this.brushModel_.getColor().substring(1);
+  var color = new ink.Color(parseInt(colorString, 16));
+  // Using this alpha channel would make calligraphy or marker brushes
+  // translucent; highlighter, watercolor, and airbrush have hard-coded alpha
+  // values in the engine.
+  color.a = 0xFF;  // Set alpha to opaque
+
+  this.engine_.brushUpdate(
+      color.getRgbaUint32(), strokeWidth, tool_type, brush_type);
+};
+
+
+/**
+ * Handles a new element created in the engine.
+ * @param {string} uuid
+ * @param {string} encodedElement
+ * @param {string} encodedTransform
+ * @override
+ */
+ink.CanvasManager.prototype.onElementCreated = function(
+    uuid, encodedElement, encodedTransform) {
+  this.pendingUUIDs_.add(uuid);
+  this.dispatchEvent(new ink.embed.events.ElementCreatedEvent(
+      uuid, encodedElement, encodedTransform));
+};
+
+
+/**
+ * Handles an element being transformed.
+ * @param {Array.<string>} uuids
+ * @param {Array.<string>} encodedTransforms
+ * @override
+ */
+ink.CanvasManager.prototype.onElementsMutated = function(
+    uuids, encodedTransforms) {
+  this.dispatchEvent(
+      new ink.embed.events.ElementsMutatedEvent(uuids, encodedTransforms));
+};
+
+
+/**
+ * Handles elements being removed.
+ * @param {Array.<string>} uuids
+ * @override
+ */
+ink.CanvasManager.prototype.onElementsRemoved = function(uuids) {
+  this.dispatchEvent(new ink.embed.events.ElementsRemovedEvent(uuids));
+};
+
+
+/**
+ * Handle an addElement request from Brix.  If this is a remote add or an
+ * add-by-undo or redo, adds the element to the engine and the local list of
+ * elements (this.UUIDs_) at the specified index.  If this is a local add
+ * originated by the engine, the element is already present in the engine and is
+ * simply removed from this.pendingUUIDs_.
+ *
+ * @param {!Object<string, string>} bundle
+ * @param {number} idx index to add the element at
+ * @param {boolean} isLocal
+ */
+ink.CanvasManager.prototype.addElement = function(bundle, idx, isLocal) {
+  if (!bundle['id']) {
+    bundle['id'] = 'local-' + this.nextLocalId_++;
+  }
+
+  var uuid = bundle['id'];
+  goog.array.insertAt(this.UUIDs_, uuid, idx);
+
+  // If the element originated in the engine, it should be present in the
+  // pending UUID set, so we just remove it from that set so that future adds by
+  // undo/redo will work as expected.
+  if (this.pendingUUIDs_.contains(uuid)) {
+    this.pendingUUIDs_.remove(uuid);
+  } else {
+    // If the element is a remote add or an add by undo or redo, it may not be
+    // the top element, in which case we add it below the element after it.
+    if (idx < this.UUIDs_.length - 1) {
+      this.engine_.addElementBelow(bundle, this.UUIDs_[idx + 1]);
+    } else {
+      this.engine_.addElement(bundle);
+    }
+  }
+
+  // TODO(wfurr): Figure out how to have the engine wake itself up.
+  // See b/18830720.
+  this.engine_.poke();
+};
+
+
+/**
+ * Removes a number of elements.
+ * @param {number} idx index to start removing.
+ * @param {number} count number of items to remove.
+ */
+ink.CanvasManager.prototype.removeElements = function(idx, count) {
+  for (var i = 0; i < count; i++) {
+    var uuid = this.UUIDs_[idx];
+    this.engine_.removeElement(uuid);
+    goog.array.removeAt(this.UUIDs_, idx);
+  }
+
+  // TODO(wfurr): Figure out how to have the engine wake itself up.
+  // See b/18830720.
+  this.engine_.poke();
+};
+
+
+/**
+ * Resets the engine but does not dispatch any Brix related events. Used to
+ * clear the canvas to reuse it to display another drawing.
+ */
+ink.CanvasManager.prototype.resetCanvas = function() {
+  this.engine_.clear();
+  this.engine_.setBackgroundColor(ink.Color.DEFAULT_BACKGROUND_COLOR);
+  this.pendingUUIDs_.clear();
+  this.UUIDs_ = [];
+  this.engine_.poke();
+};
+
+
+/**
+ * Clears the canvas.
+ */
+ink.CanvasManager.prototype.clear = function() {
+  this.engine_.removeAll();
+};
+
+
+/**
+ * Sets element transforms.
+ * @param {Array.<string>} uuids
+ * @param {Array.<string>} encodedTransforms
+ */
+ink.CanvasManager.prototype.setElementTransforms = function(
+    uuids, encodedTransforms) {
+  this.engine_.setElementTransforms(uuids, encodedTransforms);
+};
+
+
+/**
+ * Set callback flags
+ * @param {!sketchology.proto.SetCallbackFlags} setCallbackFlags
+ */
+ink.CanvasManager.prototype.setCallbackFlags = function(setCallbackFlags) {
+  this.engine_.setCallbackFlags(setCallbackFlags);
+};
+
+
+/**
+ * Sets the size of the page.
+ * @param {number} left
+ * @param {number} top
+ * @param {number} right
+ * @param {number} bottom
+ */
+ink.CanvasManager.prototype.setPageBounds = function(left, top, right, bottom) {
+  this.engine_.setPageBounds(left, top, right, bottom);
+};
+
+
+/**
+ * Deselects anything selected with the edit tool.
+ */
+ink.CanvasManager.prototype.deselectAll = function() {
+  this.engine_.deselectAll();
+};
+
+
+/**
+ * Sets the border image.
+ * @private
+ */
+ink.CanvasManager.prototype.setBorderImage_ = function() {
+  var self = this;
+  var uri = 'sketchology://border0';
+  var borderImageProto = new sketchology.proto.Border();
+  borderImageProto.setUri(uri);
+  borderImageProto.setScale(1);
+
+  ink.util.getImageBytes(ink.CanvasManager.BORDER_IMAGE, function(data, size) {
+    self.engine_.setBorderImage(
+        data, size, uri, borderImageProto,
+        ink.CanvasManager.OUT_OF_BOUNDS_COLOR);
+  });
+};
+
+
+/**
+ * Dispatches a FATAL_ERROR event, and throws an Error if it isn't handled.
+ * @param {Error=} opt_cause
+ * @private
+ */
+ink.CanvasManager.prototype.dispatchFatalError_ = function(opt_cause) {
+  if (this.dispatchEvent(new ink.embed.events.FatalErrorEvent(opt_cause))) {
+    // Unless one of the listeners returns false or preventDefaults, throw an
+    // error to trigger default exception handlers on the page.
+    throw opt_cause || new Error('Unhandled fatal ink error');
+  }
+};
+
+
+/**
+ * Enable or disable an engine flag.
+ * @param {sketchology.proto.Flag} which
+ * @param {boolean} enable
+ */
+ink.CanvasManager.prototype.assignFlag = function(which, enable) {
+  this.engine_.assignFlag(which, enable);
+};
+
+
+/**
+ * Simple undo. This only works if the SEngine was constructed with a
+ *   SingleUserDocument with InMemoryStorage.
+ */
+ink.CanvasManager.prototype.undo = function() {
+  this.engine_.undo();
+};
+
+
+/**
+ * Simple redo. This only works if the SEngine was constructed with a
+ *   SingleUserDocument with InMemoryStorage.
+ */
+ink.CanvasManager.prototype.redo = function() {
+  this.engine_.redo();
+};
+
+
+/**
+ * Returns the current snapshot.
+ * @param {function(!sketchology.proto.Snapshot)} callback
+ */
+ink.CanvasManager.prototype.getSnapshot = function(callback) {
+  this.engine_.getSnapshot(callback);
+};
+
+
+/**
+ * Loads a document from a snapshot.
+ *
+ * @param {!sketchology.proto.Snapshot} snapshotProto
+ */
+ink.CanvasManager.prototype.loadFromSnapshot = function(snapshotProto) {
+  this.engine_.loadFromSnapshot(snapshotProto);
+};
+
+
+/**
+ * Allows the user to execute arbitrary commands on the engine.
+ * @param {!sketchology.proto.Command} command
+ */
+ink.CanvasManager.prototype.handleCommand = function(command) {
+  this.engine_.handleCommand(command);
+};
+
+
+/**
+ * Gets the raw engine object. Do not use this.
+ * @return {Object}
+ */
+ink.CanvasManager.prototype.getRawEngineObject = function() {
+  return this.engine_.getRawEngineObject();
+};
+
+
+/**
+ * Generates a snapshot based on a brix document.
+ * @param {!ink.util.RealtimeDocument} brixDoc
+ * @param {function(!sketchology.proto.Snapshot)} callback
+ */
+ink.CanvasManager.prototype.convertBrixDocumentToSnapshot =
+    function(brixDoc, callback) {
+  this.engine_.convertBrixDocumentToSnapshot(brixDoc, callback);
+};
+
+
+/**
+ * @param {!sketchology.proto.Snapshot} snapshot
+ * @param {function(boolean)} callback
+ */
+ink.CanvasManager.prototype.snapshotHasPendingMutations =
+    function(snapshot, callback) {
+  this.engine_.snapshotHasPendingMutations(snapshot, callback);
+};
+
+
+/**
+ * @param {!sketchology.proto.Snapshot} snapshot
+ * @param {function(sketchology.proto.MutationPacket)} callback
+ */
+ink.CanvasManager.prototype.extractMutationPacket =
+    function(snapshot, callback) {
+  this.engine_.extractMutationPacket(snapshot, callback);
+};
+
+
+/**
+ * @param {!sketchology.proto.Snapshot} snapshot
+ * @param {function(sketchology.proto.Snapshot)} callback
+ */
+ink.CanvasManager.prototype.clearPendingMutations =
+    function(snapshot, callback) {
+  this.engine_.clearPendingMutations(snapshot, callback);
+};
+
+
+/**
+ * Calls the given callback once all previous asynchronous engine operations
+ * have been applied.
+ * @param {!Function} callback
+ */
+ink.CanvasManager.prototype.flush = function(callback) {
+  this.engine_.flush(callback);
+};
diff --git a/third_party/ink/ink/web/js/cursor_updater.js b/third_party/ink/ink/web/js/cursor_updater.js
new file mode 100644
index 0000000..5f4803e
--- /dev/null
+++ b/third_party/ink/ink/web/js/cursor_updater.js
@@ -0,0 +1,124 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+goog.provide('ink.CursorUpdater');
+
+goog.require('goog.ui.Component');
+goog.require('ink.BrushModel');
+goog.require('ink.Color');
+goog.require('ink.embed.events');
+goog.require('ink.util');
+
+
+
+/**
+ * @constructor
+ * @extends {goog.ui.Component}
+ * @struct
+ */
+ink.CursorUpdater = function() {
+  ink.CursorUpdater.base(this, 'constructor');
+
+  /** @private {ink.BrushModel} */
+  this.brushModel_ = null;
+};
+goog.inherits(ink.CursorUpdater, goog.ui.Component);
+
+
+/** @override */
+ink.CursorUpdater.prototype.enterDocument = function() {
+  ink.CursorUpdater.base(this, 'enterDocument');
+
+  this.brushModel_ = ink.BrushModel.getInstance(this);
+
+  var handler = this.getHandler();
+
+  handler.listen(this.brushModel_,
+      ink.BrushModel.EventType.CHANGE,
+      this.updateCursor_);
+
+  handler.listen(ink.util.getRootParentComponent(this),
+      ink.embed.events.EventType.DONE_LOADING,
+      this.handleDoneLoadingEvent_);
+};
+
+
+/**
+ * @param {!ink.embed.events.DoneLoadingEvent} evt
+ * @private
+ */
+ink.CursorUpdater.prototype.handleDoneLoadingEvent_ = function(evt) {
+  if (evt.isReadOnly) {
+    // If we aren't editable, use a default cursor.
+    this.getElement().style.cursor = '';
+  } else {
+    this.updateCursor_();
+  }
+};
+
+
+/**
+ * Updates the cursor icon for the drawable area based on the current selection.
+ * @private
+ */
+ink.CursorUpdater.prototype.updateCursor_ = function() {
+  var rgb = this.brushModel_.getActiveColorNumericRbg();
+  var r = 8;
+
+  var url = 'url(' + this.getCursorDataUrlImage(r, rgb) + ')';
+  var target = r + ' ' + r; // target is center of cursor
+  var fallback = ', auto';
+  var cursorStyle = url + target + fallback;
+
+  this.getElement().style.cursor = cursorStyle;
+};
+
+/**
+ * @param {number} radius
+ * @param {number} rgb
+ * @return {string} A data url for a cursor with the provided radius and color.
+ */
+ink.CursorUpdater.prototype.getCursorDataUrlImage = function(radius, rgb) {
+
+  // TODO(esrauch): We avoid initializing with a fixed brush width for normal
+  // rendering ahead of time since the android client has the same brush at a
+  // very large number of different radiuses. Since this is only used for
+  // cursors, we should really just precompute these as images offline and just
+  // splice in the colors.
+  var scratchCanvas =
+      /** @type {!HTMLCanvasElement} */ (document.createElement('canvas'));
+
+  var context =
+      /** @type {!CanvasRenderingContext2D} */ (scratchCanvas.getContext('2d'));
+
+  // Make the cursors opaque.
+  var color = new ink.Color(rgb | 0xFF000000);
+
+  // Cap the minimum radius at 2.
+  radius = Math.max(radius, 2);
+  var diameter = Math.ceil(2 * radius);
+  scratchCanvas.width = diameter;
+  scratchCanvas.height = diameter;
+
+  // If we have a dark color, use a white outline. For a light color, use a
+  // black outline.
+  // Compute the lightness value as defined by HSL.
+  var max = Math.max(color.r, color.g, color.b);
+  var min = Math.min(color.r, color.g, color.b);
+  var lightness = 0.5 * (max + min);
+  var outlineColor = lightness > 127 ? ink.Color.BLACK : ink.Color.WHITE;
+
+  context.fillStyle = outlineColor.getRgbString();
+  context.beginPath();
+  context.arc(radius, radius, radius, 0, 2 * Math.PI);
+  context.closePath();
+  context.fill();
+
+  context.fillStyle = color.getRgbString();
+  context.beginPath();
+  context.arc(radius, radius, radius - 1, 0, 2 * Math.PI);
+  context.closePath();
+  context.fill();
+
+  return scratchCanvas.toDataURL();
+};
diff --git a/third_party/ink/ink/web/js/embed/embed.js b/third_party/ink/ink/web/js/embed/embed.js
new file mode 100644
index 0000000..1c159444c
--- /dev/null
+++ b/third_party/ink/ink/web/js/embed/embed.js
@@ -0,0 +1,67 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+goog.provide('ink.embed.Config');
+
+goog.require('goog.ui.Component');
+goog.require('ink.util');
+goog.require('protos.research.ink.InkEvent');
+
+
+
+/**
+ * @constructor
+ * @struct
+ */
+ink.embed.Config = function() {
+  /**
+   * The parent element to render into (required).
+   * @type {?Element}
+   */
+  this.parentEl = null;
+
+  /**
+   * The parent component to set (optional);
+   * TODO(esrauch): This is only necessary because of the cross-package events
+   * that are currently going in both directions. Remove this from the config
+   * after this is cleaned up to avoid Whiteboard events being listened to
+   * directly in Embed code.
+   * @type {?goog.ui.Component}
+   */
+  this.parentComponent = null;
+
+  /**
+   * If true, allows ink to show its own error dialogs for certain cases.
+   * @type {boolean}
+   */
+  this.allowDialogs = false;
+
+  /**
+   * Path to NaCl binary.
+   *
+   * If you are using the Native Client build, you must specify the url for the
+   * Ink Native Client NMF file.
+   *
+   * @type {?string}
+   */
+  this.nativeClientManifestUrl = null;
+
+  /**
+   * The source of the embedder.
+   *
+   * From //logs/proto/research/ink/ink_event.proto
+   *
+   * @type {protos.research.ink.InkEvent.Host}
+   */
+  this.logsHost = protos.research.ink.InkEvent.Host.UNKNOWN_HOST;
+
+  /**
+   * The type of the document the SEngine should be constructed with.
+   *
+   * For Brix documents, this should be PASSTHROUGH_DOCUMENT.
+   *
+   * @type {ink.util.SEngineType}
+   */
+  this.sengineType =
+      ink.util.SEngineType.PASSTHROUGH_DOCUMENT;
+};
diff --git a/third_party/ink/ink/web/js/embed/embed_component.js b/third_party/ink/ink/web/js/embed/embed_component.js
new file mode 100644
index 0000000..60206d9
--- /dev/null
+++ b/third_party/ink/ink/web/js/embed/embed_component.js
@@ -0,0 +1,417 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+/**
+ * @fileoverview The embeddable ink component. Generally should be constructed
+ * by using {@code ink.embed.Config.execute()}.
+ */
+goog.provide('ink.embed.EmbedComponent');
+
+goog.require('goog.dom');
+goog.require('goog.events');
+goog.require('goog.events.Event');
+goog.require('goog.math.Size');
+goog.require('goog.soy');
+goog.require('goog.ui.Component');
+goog.require('ink.CanvasManager');
+goog.require('ink.Color');
+goog.require('ink.CursorUpdater');
+goog.require('ink.embed.Config');
+goog.require('ink.embed.events');
+goog.require('ink.soy.embedContent');
+goog.require('ink.util');
+goog.require('protos.research.ink.InkEvent');
+goog.require('sketchology.proto.SetCallbackFlags');
+
+
+
+/**
+ * @param {!ink.embed.Config} config
+ * @param {!Function} callback
+ * @constructor
+ * @extends {goog.ui.Component}
+ * @struct
+ */
+ink.embed.EmbedComponent = function(config, callback) {
+  ink.embed.EmbedComponent.base(this, 'constructor');
+
+  /** @private {!ink.embed.Config} */
+  this.config_ = config;
+
+  /** @public {boolean} */
+  this.allowDialogs = config.allowDialogs;
+
+  /** @private {!ink.CursorUpdater} */
+  this.cursorUpdater_ = new ink.CursorUpdater();
+  this.addChild(this.cursorUpdater_);
+
+  /** @private {!ink.CanvasManager} */
+  this.canvasManager_ =
+      new ink.CanvasManager(config.nativeClientManifestUrl, config.sengineType);
+  this.addChild(this.canvasManager_);
+
+  /** @private {!Function} */
+  this.callback_ = callback;
+};
+goog.inherits(ink.embed.EmbedComponent, goog.ui.Component);
+
+
+////////////////////////////////////////////////////////////////
+// Public API for embedders.
+////////////////////////////////////////////////////////////////
+
+
+/**
+ * @param {!ink.embed.Config} config
+ * @param {function(ink.embed.EmbedComponent)} callback Callback function that
+ * returns the component that is configured based on the settings in this
+ * Config. This component will raise the relevant ink.embed.Events and provides
+ * an interface to change the brush color, size, etc. Null is returned if the
+ * config is invalid.
+ */
+ink.embed.EmbedComponent.execute = function(config, callback) {
+  var embed = new ink.embed.EmbedComponent(config, callback);
+  embed.setParent(config.parentComponent);
+  embed.render(config.parentEl);
+};
+
+
+/** Removes all elements from the drawing space. */
+ink.embed.EmbedComponent.prototype.clear = function() {
+  var e = new goog.events.Event(ink.embed.events.EventType.CLEAR_REQUESTED);
+  this.dispatchEvent(e);
+  if (!e.defaultPrevented) {
+    this.canvasManager_.clear();
+  }
+};
+
+
+/** Undoes the last modification to the document taken by the user. */
+ink.embed.EmbedComponent.prototype.undo = function() {
+  var e = new goog.events.Event(ink.embed.events.EventType.UNDO_REQUESTED);
+  this.dispatchEvent(e);
+  if (!e.defaultPrevented) {
+    this.canvasManager_.undo();
+  }
+  var eventProto = ink.util.createDocumentEvent(
+      this.getLogsHost(),
+      protos.research.ink.InkEvent.DocumentEvent.DocumentEventType.UNDO);
+  var logEvent = new ink.embed.events.LogEvent(eventProto);
+  this.dispatchEvent(logEvent);
+};
+
+
+/** Redoes the last undone action. */
+ink.embed.EmbedComponent.prototype.redo = function() {
+  var e = new goog.events.Event(ink.embed.events.EventType.REDO_REQUESTED);
+  this.dispatchEvent(e);
+  if (!e.defaultPrevented) {
+    this.canvasManager_.redo();
+  }
+  var eventProto = ink.util.createDocumentEvent(
+      this.getLogsHost(),
+      protos.research.ink.InkEvent.DocumentEvent.DocumentEventType.REDO);
+  var logEvent = new ink.embed.events.LogEvent(eventProto);
+  this.dispatchEvent(logEvent);
+};
+
+
+/**
+ * Adds a background image.  Sets page bounds to match the given image.
+ * @param {string} imgSrc
+ * @param {Function=} opt_cb callback on image load complete
+ */
+ink.embed.EmbedComponent.prototype.setBackgroundImage =
+    function(imgSrc, opt_cb) {
+  ink.util.getImageBytes(imgSrc,
+    (data, size) => {
+      this.canvasManager_.setBackgroundImage(data, size);
+      if (opt_cb) opt_cb();
+    });
+};
+
+
+/**
+ * Sets a background image to match the existing page bounds.  Will not
+ * display if no page bounds are set.
+ * @param {string} imgSrc
+ * @param {Function=} opt_cb callback on image load complete
+ */
+ink.embed.EmbedComponent.prototype.setImageToUseForPageBackground =
+    function(imgSrc, opt_cb) {
+  ink.util.getImageBytes(imgSrc,
+    (data, size) => {
+      this.canvasManager_.setImageToUseForPageBackground(data, size);
+      if (opt_cb) opt_cb();
+    });
+};
+
+
+/**
+ * Set background color.
+ * @param {ink.Color} color
+ */
+ink.embed.EmbedComponent.prototype.setBackgroundColor = function(color) {
+  this.canvasManager_.setBackgroundColor(color);
+};
+
+
+/**
+ * Export the scene as a PNG from the engine.
+ * @param {number} maxWidth
+ * @param {boolean} drawBackground
+ * @param {function(!goog.html.SafeUrl)} callback
+ * @param {boolean=} opt_asBlob If true, returns a blob uri.
+ */
+ink.embed.EmbedComponent.prototype.exportPng = function(
+    maxWidth, drawBackground, callback, opt_asBlob) {
+  this.canvasManager_.exportPng(maxWidth, drawBackground, callback, opt_asBlob);
+};
+
+
+/**
+ * Set callback flags, for whether to receive callbacks and what data to attach.
+ * @param {!sketchology.proto.SetCallbackFlags} setCallbackFlags
+ */
+ink.embed.EmbedComponent.prototype.setCallbackFlags =
+    function(setCallbackFlags) {
+  this.canvasManager_.setCallbackFlags(setCallbackFlags);
+};
+
+
+/**
+ * Sets the size of the page.
+ * @param {number} left
+ * @param {number} top
+ * @param {number} right
+ * @param {number} bottom
+ */
+ink.embed.EmbedComponent.prototype.setPageBounds =
+    function(left, top, right, bottom) {
+  this.canvasManager_.setPageBounds(left, top, right, bottom);
+};
+
+
+/**
+ * Deselects anything selected with the edit tool.
+ */
+ink.embed.EmbedComponent.prototype.deselectAll = function() {
+  this.canvasManager_.deselectAll();
+};
+
+
+////////////////////////////////////////////////////////////////
+// Internal code.
+////////////////////////////////////////////////////////////////
+
+
+/** @override */
+ink.embed.EmbedComponent.prototype.createDom = function() {
+  this.setElementInternal(goog.soy.renderAsElement(ink.soy.embedContent));
+};
+
+
+/** @override */
+ink.embed.EmbedComponent.prototype.enterDocument = function() {
+  ink.embed.EmbedComponent.base(this, 'enterDocument');
+
+  var container = goog.dom.getElement('layer-container');
+
+  this.canvasManager_.render(container);
+  this.cursorUpdater_.decorate(container);
+
+  this.getHandler().listen(
+      this.canvasManager_, ink.embed.events.EventType.CANVAS_INITIALIZED,
+      goog.bind(this.callback_, this, this));
+};
+
+
+/**
+ * Manually adds an element.
+ * @param {!sketchology.proto.Element} elem
+ * @param {number} idx index to add the element at
+ * @param {boolean} isLocal
+ */
+ink.embed.EmbedComponent.prototype.addElement = function(elem, idx, isLocal) {
+  this.canvasManager_.addElement(elem, idx, isLocal);
+};
+
+
+/**
+ * Manually removes a number of elements.
+ * @param {number} idx index to start removing.
+ * @param {number} count number of items to remove.
+ */
+ink.embed.EmbedComponent.prototype.removeElements = function(idx, count) {
+  this.canvasManager_.removeElements(idx, count);
+};
+
+
+/**
+ * Resets the canvas associated with the embed component.
+ *
+ * Note: Does not affect any attached Brix documents.
+ */
+ink.embed.EmbedComponent.prototype.resetCanvas = function() {
+  this.canvasManager_.resetCanvas();
+};
+
+
+/**
+ * Sets or unsets readOnly on the canvas.
+ * @param {boolean} readOnly
+ */
+ink.embed.EmbedComponent.prototype.setReadOnly = function(readOnly) {
+  this.canvasManager_.setReadOnly(readOnly);
+};
+
+
+/**
+ * Sets element transforms.
+ * @param {Array.<string>} uuids
+ * @param {Array.<string>} encodedTransforms
+ */
+ink.embed.EmbedComponent.prototype.setElementTransforms = function(
+    uuids, encodedTransforms) {
+  this.canvasManager_.setElementTransforms(uuids, encodedTransforms);
+};
+
+
+/**
+ * Returns true if the document is empty, false if it has content, and
+ *   undefined if not a brix document.
+ * @param {Function} callback
+ */
+ink.embed.EmbedComponent.prototype.isEmpty = function(callback) {
+  var e = new ink.embed.events.EmptyStatusRequestedEvent(callback);
+  this.dispatchEvent(e);
+  if (!e.defaultPrevented) {
+    callback(undefined);
+  }
+};
+
+
+/**
+ * Returns the current dimensions of the canvas element.
+ * @return {goog.math.Size} The width and height of the canvas.
+ */
+ink.embed.EmbedComponent.prototype.getCanvasDimensions = function() {
+  var element =
+      this.canvasManager_.getElementStrict().querySelector('canvas,embed');
+  return new goog.math.Size(element.clientWidth, element.clientHeight);
+};
+
+
+/**
+ * Returns the logs host id.
+ * @return {protos.research.ink.InkEvent.Host}
+ */
+ink.embed.EmbedComponent.prototype.getLogsHost = function() {
+  return this.config_.logsHost;
+};
+
+
+/**
+ * Enable or disable an engine flag.
+ * @param {sketchology.proto.Flag} which
+ * @param {boolean} enable
+ */
+ink.embed.EmbedComponent.prototype.assignFlag = function(which, enable) {
+  this.canvasManager_.assignFlag(which, enable);
+};
+
+
+/**
+ * Returns the current snapshot. Only works if sengineType is set to
+ *   ink.util.SEngineType.IN_MEMORY.
+ * @param {function(!sketchology.proto.Snapshot)} callback
+ */
+ink.embed.EmbedComponent.prototype.getSnapshot = function(callback) {
+  if (this.config_.sengineType !== ink.util.SEngineType.IN_MEMORY) {
+    throw new Error(`Can't getSnapshot without sengineType IN_MEMORY.`);
+  }
+  this.canvasManager_.getSnapshot(callback);
+};
+
+
+/**
+ * Loads a document from a snapshot. Only works if sengineType is set to
+ *   ink.util.SEngineType.IN_MEMORY.
+ *
+ * @param {!sketchology.proto.Snapshot} snapshotProto
+ */
+ink.embed.EmbedComponent.prototype.loadFromSnapshot = function(snapshotProto) {
+  if (this.config_.sengineType !== ink.util.SEngineType.IN_MEMORY) {
+    throw new Error(`Can't loadFromSnapshot without sengineType IN_MEMORY.`);
+  }
+  this.canvasManager_.loadFromSnapshot(snapshotProto);
+};
+
+
+/**
+ * Allows the user to execute arbitrary commands on the engine.
+ * @param {!sketchology.proto.Command} command
+ */
+ink.embed.EmbedComponent.prototype.handleCommand = function(command) {
+  this.canvasManager_.handleCommand(command);
+};
+
+
+/**
+ * Gets the raw engine object. Do not use this.
+ * @return {Object}
+ */
+ink.embed.EmbedComponent.prototype.getRawEngineObject = function() {
+  return this.canvasManager_.getRawEngineObject();
+};
+
+
+/**
+ * Generates a snapshot based on a brix document.
+ * @param {!ink.util.RealtimeDocument} brixDoc
+ * @param {function(!sketchology.proto.Snapshot)} callback
+ */
+ink.embed.EmbedComponent.prototype.convertBrixDocumentToSnapshot =
+    function(brixDoc, callback) {
+  this.canvasManager_.convertBrixDocumentToSnapshot(brixDoc, callback);
+};
+
+
+/**
+ * @param {!sketchology.proto.Snapshot} snapshot
+ * @param {function(boolean)} callback
+ */
+ink.embed.EmbedComponent.prototype.snapshotHasPendingMutations =
+    function(snapshot, callback) {
+  this.canvasManager_.snapshotHasPendingMutations(snapshot, callback);
+};
+
+
+/**
+ * @param {!sketchology.proto.Snapshot} snapshot
+ * @param {function(sketchology.proto.MutationPacket)} callback
+ */
+ink.embed.EmbedComponent.prototype.extractMutationPacket =
+    function(snapshot, callback) {
+  this.canvasManager_.extractMutationPacket(snapshot, callback);
+};
+
+
+/**
+ * @param {!sketchology.proto.Snapshot} snapshot
+ * @param {function(sketchology.proto.Snapshot)} callback
+ */
+ink.embed.EmbedComponent.prototype.clearPendingMutations =
+    function(snapshot, callback) {
+  this.canvasManager_.clearPendingMutations(snapshot, callback);
+};
+
+
+/**
+ * Calls the given callback once all previous asynchronous engine operations
+ * have been applied.
+ * @param {!Function} callback
+ */
+ink.embed.EmbedComponent.prototype.flush = function(callback) {
+  this.canvasManager_.flush(callback);
+};
diff --git a/third_party/ink/ink/web/js/embed/events.js b/third_party/ink/ink/web/js/embed/events.js
new file mode 100644
index 0000000..2997c5e
--- /dev/null
+++ b/third_party/ink/ink/web/js/embed/events.js
@@ -0,0 +1,225 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+/**
+ * @fileoverview Events that embedders can listen to. Note: currently embedders
+ * are able to listen to other internal events, but only these events should be
+ * treated as a public API.
+ */
+goog.provide('ink.embed.events');
+goog.provide('ink.embed.events.DoneLoadingEvent');
+goog.provide('ink.embed.events.ElementCreatedEvent');
+goog.provide('ink.embed.events.ElementsMutatedEvent');
+goog.provide('ink.embed.events.ElementsRemovedEvent');
+goog.provide('ink.embed.events.EmptyStatusRequestedEvent');
+goog.provide('ink.embed.events.EventType');
+goog.provide('ink.embed.events.FatalErrorEvent');
+goog.provide('ink.embed.events.LogEvent');
+
+goog.require('goog.events');
+goog.require('protos.research.ink.InkEvent');
+
+
+/** @enum {string} */
+ink.embed.events.EventType = {
+  // Dispatched when the space is ready to be drawn onto.
+  DONE_LOADING: goog.events.getUniqueId('done_loading'),
+
+  // Dispatched when pen mode is enabled.
+  PEN_MODE_ENABLED: goog.events.getUniqueId('pen_mode_enabled'),
+
+  // Dispatched when the GL canvas is initialized.
+  CANVAS_INITIALIZED: goog.events.getUniqueId('canvas_initialized'),
+
+  // Dispatched when a new element has been created on the canvas.
+  ELEMENT_CREATED: goog.events.getUniqueId('element_created'),
+
+  // Dispatched when an element is mutaed.
+  ELEMENTS_MUTATED: goog.events.getUniqueId('elements_mutated'),
+
+  // Dispatched when elements are removed.
+  ELEMENTS_REMOVED: goog.events.getUniqueId('elements_removed'),
+
+  // Dispatched when something wants to know if the document is empty.
+  EMPTY_STATUS_REQUESTED: goog.events.getUniqueId('empty_status_requested'),
+
+  // Dispatched when something needs to be logged.
+  LOG: goog.events.getUniqueId('log'),
+
+  // Dispatched to request an undo.
+  UNDO_REQUESTED: goog.events.getUniqueId('undo_requested'),
+
+  // Dispatched to request a redo.
+  REDO_REQUESTED: goog.events.getUniqueId('redo_requested'),
+
+  // Dispatched to request a clear.
+  CLEAR_REQUESTED: goog.events.getUniqueId('clear_requested'),
+
+  // Dispatched when a fatal error occurs and the canvas is no longer valid.
+  // If this is not handled, an Error is thrown (or re-raised).  The canvas
+  // should be discarded and a new one constructed.
+  FATAL_ERROR: goog.events.getUniqueId('fatal_error')
+};
+
+
+
+/**
+ * An event fired when the embed is ready to be drawn onto.
+ *
+ * @param {Object} brixDoc The brix realtime document associated with
+ * the document that has been loaded.
+ * @param {boolean} isReadOnly Whether the document is read only.
+ *
+ * @extends {goog.events.Event}
+ * @constructor
+ * @struct
+ */
+ink.embed.events.DoneLoadingEvent = function(brixDoc, isReadOnly) {
+  ink.embed.events.DoneLoadingEvent.base(
+      this, 'constructor', ink.embed.events.EventType.DONE_LOADING);
+
+  /** @type {Object} */
+  this.brixDoc = brixDoc;
+
+  /** @type {boolean} */
+  this.isReadOnly = isReadOnly;
+};
+goog.inherits(ink.embed.events.DoneLoadingEvent, goog.events.Event);
+
+
+/**
+ * An event fired when pen mode is enabled or disabled.
+ *
+ * @param {boolean} enabled
+ *
+ * @extends {goog.events.Event}
+ * @constructor
+ * @struct
+ */
+ink.embed.events.PenModeEnabled = function(enabled) {
+  ink.embed.events.PenModeEnabled.base(this, 'constructor',
+      ink.embed.events.EventType.PEN_MODE_ENABLED);
+
+  /** @type {boolean} */
+  this.enabled = enabled;
+};
+goog.inherits(ink.embed.events.PenModeEnabled, goog.events.Event);
+
+
+/**
+ * An event fired when a new element is created in the embed.
+ *
+ * @param {string} uuid
+ * @param {string} encodedElement
+ * @param {string} encodedTransform
+ *
+ * @extends {goog.events.Event}
+ * @constructor
+ * @struct
+ */
+ink.embed.events.ElementCreatedEvent = function(uuid, encodedElement,
+    encodedTransform) {
+  ink.embed.events.ElementCreatedEvent.base(
+      this, 'constructor', ink.embed.events.EventType.ELEMENT_CREATED);
+
+  this.uuid = uuid;
+  this.encodedElement = encodedElement;
+  this.encodedTransform = encodedTransform;
+};
+goog.inherits(ink.embed.events.ElementCreatedEvent, goog.events.Event);
+
+
+/**
+ * An event fired when an element is mutated.
+ *
+ * @param {Array.<string>} uuids
+ * @param {Array.<string>} encodedTransforms
+ *
+ * @extends {goog.events.Event}
+ * @constructor
+ * @struct
+ */
+ink.embed.events.ElementsMutatedEvent = function(uuids, encodedTransforms) {
+  ink.embed.events.ElementsMutatedEvent.base(
+      this, 'constructor', ink.embed.events.EventType.ELEMENTS_MUTATED);
+
+  this.uuids = uuids;
+  this.encodedTransforms = encodedTransforms;
+};
+goog.inherits(ink.embed.events.ElementsMutatedEvent, goog.events.Event);
+
+
+/**
+ * An event fired when elements are removed.
+ *
+ * @param {Array.<string>} uuids
+ *
+ * @extends {goog.events.Event}
+ * @constructor
+ * @struct
+ */
+ink.embed.events.ElementsRemovedEvent = function(uuids) {
+  ink.embed.events.ElementsRemovedEvent.base(
+      this, 'constructor', ink.embed.events.EventType.ELEMENTS_REMOVED);
+
+  this.uuids = uuids;
+};
+goog.inherits(ink.embed.events.ElementsRemovedEvent, goog.events.Event);
+
+
+/**
+ * An event fired when something wants to know if the document is empty.
+ *
+ * @param {Function} callback
+ *
+ * @extends {goog.events.Event}
+ * @constructor
+ * @struct
+ */
+ink.embed.events.EmptyStatusRequestedEvent = function(callback) {
+  ink.embed.events.EmptyStatusRequestedEvent.base(
+      this, 'constructor', ink.embed.events.EventType.EMPTY_STATUS_REQUESTED);
+
+  this.callback = callback;
+};
+goog.inherits(ink.embed.events.EmptyStatusRequestedEvent, goog.events.Event);
+
+
+/**
+ * An event fired when an event should be logged by the embedder.
+ *
+ * @param {protos.research.ink.InkEvent} proto The ink event proto.
+ *
+ * @extends {goog.events.Event}
+ * @constructor
+ * @struct
+ */
+ink.embed.events.LogEvent = function(proto) {
+  ink.embed.events.LogEvent.base(
+      this, 'constructor', ink.embed.events.EventType.LOG);
+
+  this.proto = proto;
+};
+goog.inherits(ink.embed.events.LogEvent, goog.events.Event);
+
+
+/**
+ * An event fired when a fatal error has occured.
+ *
+ * If this error is not handled by the embedder, the component will throw an
+ * error.  The embedder should discard this component and construct a new one.
+ *
+ * @param {Error=} opt_cause Optional cause of the fatal error
+ *
+ * @extends {goog.events.Event}
+ * @constructor
+ * @struct
+ */
+ink.embed.events.FatalErrorEvent = function(opt_cause) {
+  ink.embed.events.FatalErrorEvent.base(
+      this, 'constructor', ink.embed.events.EventType.FATAL_ERROR);
+
+  /** @type {Error} */
+  this.cause = opt_cause || null;
+};
+goog.inherits(ink.embed.events.FatalErrorEvent, goog.events.Event);
diff --git a/third_party/ink/ink/web/js/main.soy.js b/third_party/ink/ink/web/js/main.soy.js
new file mode 100644
index 0000000..3883d56
--- /dev/null
+++ b/third_party/ink/ink/web/js/main.soy.js
@@ -0,0 +1,31 @@
+// Copyright 2017 The Chromium Authors. 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 was automatically generated from main.soy.
+// Please don't edit this file by hand.
+
+/**
+ * @fileoverview Templates in namespace ink.soy.
+ * @public
+ */
+
+goog.provide('ink.soy.embedContent');
+
+goog.require('soy');
+goog.require('soydata.VERY_UNSAFE');
+
+
+/**
+ * @param {Object<string, *>=} opt_data
+ * @param {Object<string, *>=} opt_ijData
+ * @param {Object<string, *>=} opt_ijData_deprecated
+ * @return {!goog.soy.data.SanitizedHtml}
+ * @suppress {checkTypes}
+ */
+ink.soy.embedContent = function(opt_data, opt_ijData, opt_ijData_deprecated) {
+  opt_ijData = opt_ijData_deprecated || opt_ijData;
+  return soydata.VERY_UNSAFE.ordainSanitizedHtml(((goog.DEBUG && soy.$$debugSoyTemplateInfo) ? '<!--dta_of(ink.soy.embedContent, research/ink/web/js/main.soy, 7)-->' : '') + '<div id="canvas-parent"><style' + (opt_ijData && opt_ijData.csp_nonce ? ' nonce="' + soy.$$escapeHtmlAttribute(opt_ijData && opt_ijData.csp_nonce) + '"' : '') + '>\n        #canvas-parent {\n          height: 100%;\n          position: relative;\n          width: 100%;\n        }\n        #layer-container {\n          height: 100%;\n          position: relative;\n          width: 100%;\n        }\n        #ink-engine {\n          height: 100%;\n          left: 0;\n          position: absolute;\n          top: 0;\n          width: 100%;\n          touch-action: none;\n        }\n        .above-ink-canvas {\n          display: none;\n        }\n      </style><div class="above-ink-canvas"></div><div id="layer-container"></div><div class="below-ink-canvas"></div></div>' + ((goog.DEBUG && soy.$$debugSoyTemplateInfo) ? '<!--dta_cf(ink.soy.embedContent)-->' : ''));
+};
+if (goog.DEBUG) {
+  ink.soy.embedContent.soyTemplateName = 'ink.soy.embedContent';
+}
diff --git a/third_party/ink/ink_event.pb.js b/third_party/ink/ink_event.pb.js
new file mode 100644
index 0000000..26cd567
--- /dev/null
+++ b/third_party/ink/ink_event.pb.js
@@ -0,0 +1,2654 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// Protocol Buffer 2 Copyright 2008 Google Inc.
+// All other code copyright its respective owners.
+
+/**
+ * @fileoverview Generated Protocol Buffer code for file
+ * logs/proto/research/ink/ink_event.proto.
+ * Generated by //net/proto2/compiler/public:protocol_compiler.
+ * @suppress {messageConventions} 
+ */
+
+goog.provide('protos.research.ink.InkEvent');
+goog.provide('protos.research.ink.InkEvent.DocumentEvent');
+goog.provide('protos.research.ink.InkEvent.DocumentEvent.OpenedEvent');
+goog.provide('protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent');
+goog.provide('protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined');
+goog.provide('protos.research.ink.InkEvent.DocumentEvent.DocumentState');
+goog.provide('protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField');
+goog.provide('protos.research.ink.InkEvent.DocumentEvent.DocumentEventType');
+goog.provide('protos.research.ink.InkEvent.ToolbarEvent');
+goog.provide('protos.research.ink.InkEvent.ToolbarEvent.ToolEventType');
+goog.provide('protos.research.ink.InkEvent.ToolbarEvent.ToolType');
+goog.provide('protos.research.ink.InkEvent.ToolbarEvent.ExpandMethod');
+goog.provide('protos.research.ink.InkEvent.EngineEvent');
+goog.provide('protos.research.ink.InkEvent.EngineEvent.EngineEventType');
+goog.provide('protos.research.ink.InkEvent.GmsEvent');
+goog.provide('protos.research.ink.InkEvent.GmsEvent.GmsEventType');
+goog.provide('protos.research.ink.InkEvent.Host');
+goog.provide('protos.research.ink.InkEvent.EventType');
+
+goog.require('goog.proto2.Message');
+
+
+
+/**
+ * Message InkEvent.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+protos.research.ink.InkEvent = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(protos.research.ink.InkEvent, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+protos.research.ink.InkEvent.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!protos.research.ink.InkEvent} The cloned message.
+ * @override
+ */
+protos.research.ink.InkEvent.prototype.clone;
+
+
+/**
+ * Gets the value of the host field.
+ * @return {?protos.research.ink.InkEvent.Host} The value.
+ */
+protos.research.ink.InkEvent.prototype.getHost = function() {
+  return /** @type {?protos.research.ink.InkEvent.Host} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the host field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.Host} The value.
+ */
+protos.research.ink.InkEvent.prototype.getHostOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.Host} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the host field.
+ * @param {!protos.research.ink.InkEvent.Host} value The value.
+ */
+protos.research.ink.InkEvent.prototype.setHost = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the host field has a value.
+ */
+protos.research.ink.InkEvent.prototype.hasHost = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the host field.
+ */
+protos.research.ink.InkEvent.prototype.hostCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the host field.
+ */
+protos.research.ink.InkEvent.prototype.clearHost = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the event_type field.
+ * @return {?protos.research.ink.InkEvent.EventType} The value.
+ */
+protos.research.ink.InkEvent.prototype.getEventType = function() {
+  return /** @type {?protos.research.ink.InkEvent.EventType} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the event_type field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.EventType} The value.
+ */
+protos.research.ink.InkEvent.prototype.getEventTypeOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.EventType} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the event_type field.
+ * @param {!protos.research.ink.InkEvent.EventType} value The value.
+ */
+protos.research.ink.InkEvent.prototype.setEventType = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the event_type field has a value.
+ */
+protos.research.ink.InkEvent.prototype.hasEventType = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the event_type field.
+ */
+protos.research.ink.InkEvent.prototype.eventTypeCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the event_type field.
+ */
+protos.research.ink.InkEvent.prototype.clearEventType = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the document_event field.
+ * @return {?protos.research.ink.InkEvent.DocumentEvent} The value.
+ */
+protos.research.ink.InkEvent.prototype.getDocumentEvent = function() {
+  return /** @type {?protos.research.ink.InkEvent.DocumentEvent} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the document_event field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.DocumentEvent} The value.
+ */
+protos.research.ink.InkEvent.prototype.getDocumentEventOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.DocumentEvent} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the document_event field.
+ * @param {!protos.research.ink.InkEvent.DocumentEvent} value The value.
+ */
+protos.research.ink.InkEvent.prototype.setDocumentEvent = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the document_event field has a value.
+ */
+protos.research.ink.InkEvent.prototype.hasDocumentEvent = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the document_event field.
+ */
+protos.research.ink.InkEvent.prototype.documentEventCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the document_event field.
+ */
+protos.research.ink.InkEvent.prototype.clearDocumentEvent = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the toolbar_event field.
+ * @return {?protos.research.ink.InkEvent.ToolbarEvent} The value.
+ */
+protos.research.ink.InkEvent.prototype.getToolbarEvent = function() {
+  return /** @type {?protos.research.ink.InkEvent.ToolbarEvent} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the toolbar_event field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.ToolbarEvent} The value.
+ */
+protos.research.ink.InkEvent.prototype.getToolbarEventOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.ToolbarEvent} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the toolbar_event field.
+ * @param {!protos.research.ink.InkEvent.ToolbarEvent} value The value.
+ */
+protos.research.ink.InkEvent.prototype.setToolbarEvent = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the toolbar_event field has a value.
+ */
+protos.research.ink.InkEvent.prototype.hasToolbarEvent = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the toolbar_event field.
+ */
+protos.research.ink.InkEvent.prototype.toolbarEventCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the toolbar_event field.
+ */
+protos.research.ink.InkEvent.prototype.clearToolbarEvent = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Gets the value of the engine_event field.
+ * @return {?protos.research.ink.InkEvent.EngineEvent} The value.
+ */
+protos.research.ink.InkEvent.prototype.getEngineEvent = function() {
+  return /** @type {?protos.research.ink.InkEvent.EngineEvent} */ (this.get$Value(5));
+};
+
+
+/**
+ * Gets the value of the engine_event field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.EngineEvent} The value.
+ */
+protos.research.ink.InkEvent.prototype.getEngineEventOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.EngineEvent} */ (this.get$ValueOrDefault(5));
+};
+
+
+/**
+ * Sets the value of the engine_event field.
+ * @param {!protos.research.ink.InkEvent.EngineEvent} value The value.
+ */
+protos.research.ink.InkEvent.prototype.setEngineEvent = function(value) {
+  this.set$Value(5, value);
+};
+
+
+/**
+ * @return {boolean} Whether the engine_event field has a value.
+ */
+protos.research.ink.InkEvent.prototype.hasEngineEvent = function() {
+  return this.has$Value(5);
+};
+
+
+/**
+ * @return {number} The number of values in the engine_event field.
+ */
+protos.research.ink.InkEvent.prototype.engineEventCount = function() {
+  return this.count$Values(5);
+};
+
+
+/**
+ * Clears the values in the engine_event field.
+ */
+protos.research.ink.InkEvent.prototype.clearEngineEvent = function() {
+  this.clear$Field(5);
+};
+
+
+/**
+ * Gets the value of the gms_event field.
+ * @return {?protos.research.ink.InkEvent.GmsEvent} The value.
+ */
+protos.research.ink.InkEvent.prototype.getGmsEvent = function() {
+  return /** @type {?protos.research.ink.InkEvent.GmsEvent} */ (this.get$Value(6));
+};
+
+
+/**
+ * Gets the value of the gms_event field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.GmsEvent} The value.
+ */
+protos.research.ink.InkEvent.prototype.getGmsEventOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.GmsEvent} */ (this.get$ValueOrDefault(6));
+};
+
+
+/**
+ * Sets the value of the gms_event field.
+ * @param {!protos.research.ink.InkEvent.GmsEvent} value The value.
+ */
+protos.research.ink.InkEvent.prototype.setGmsEvent = function(value) {
+  this.set$Value(6, value);
+};
+
+
+/**
+ * @return {boolean} Whether the gms_event field has a value.
+ */
+protos.research.ink.InkEvent.prototype.hasGmsEvent = function() {
+  return this.has$Value(6);
+};
+
+
+/**
+ * @return {number} The number of values in the gms_event field.
+ */
+protos.research.ink.InkEvent.prototype.gmsEventCount = function() {
+  return this.count$Values(6);
+};
+
+
+/**
+ * Clears the values in the gms_event field.
+ */
+protos.research.ink.InkEvent.prototype.clearGmsEvent = function() {
+  this.clear$Field(6);
+};
+
+
+/**
+ * Enumeration Host.
+ * @enum {number}
+ */
+protos.research.ink.InkEvent.Host = {
+  UNKNOWN_HOST: 0,
+  FISHFOOD: 1,
+  KEEP: 2,
+  CLASSROOM: 3,
+  FIREBALL: 4
+};
+
+
+/**
+ * Enumeration EventType.
+ * @enum {number}
+ */
+protos.research.ink.InkEvent.EventType = {
+  UNKNOWN_TYPE: 0,
+  DOCUMENT_EVENT: 1,
+  TOOLBAR_EVENT: 2,
+  ENGINE_EVENT: 3,
+  GMS_EVENT: 4
+};
+
+
+
+/**
+ * Message DocumentEvent.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+protos.research.ink.InkEvent.DocumentEvent = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(protos.research.ink.InkEvent.DocumentEvent, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+protos.research.ink.InkEvent.DocumentEvent.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!protos.research.ink.InkEvent.DocumentEvent} The cloned message.
+ * @override
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.clone;
+
+
+/**
+ * Gets the value of the event_type field.
+ * @return {?protos.research.ink.InkEvent.DocumentEvent.DocumentEventType} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getEventType = function() {
+  return /** @type {?protos.research.ink.InkEvent.DocumentEvent.DocumentEventType} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the event_type field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.DocumentEvent.DocumentEventType} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getEventTypeOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.DocumentEvent.DocumentEventType} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the event_type field.
+ * @param {!protos.research.ink.InkEvent.DocumentEvent.DocumentEventType} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.setEventType = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the event_type field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.hasEventType = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the event_type field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.eventTypeCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the event_type field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.clearEventType = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the opened_event field.
+ * @return {?protos.research.ink.InkEvent.DocumentEvent.OpenedEvent} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getOpenedEvent = function() {
+  return /** @type {?protos.research.ink.InkEvent.DocumentEvent.OpenedEvent} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the opened_event field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.DocumentEvent.OpenedEvent} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getOpenedEventOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.DocumentEvent.OpenedEvent} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the opened_event field.
+ * @param {!protos.research.ink.InkEvent.DocumentEvent.OpenedEvent} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.setOpenedEvent = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the opened_event field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.hasOpenedEvent = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the opened_event field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.openedEventCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the opened_event field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.clearOpenedEvent = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the open_cancelled_event field.
+ * @return {?protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getOpenCancelledEvent = function() {
+  return /** @type {?protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the open_cancelled_event field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getOpenCancelledEventOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the open_cancelled_event field.
+ * @param {!protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.setOpenCancelledEvent = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the open_cancelled_event field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.hasOpenCancelledEvent = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the open_cancelled_event field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.openCancelledEventCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the open_cancelled_event field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.clearOpenCancelledEvent = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the error_code field.
+ * @return {?string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getErrorCode = function() {
+  return /** @type {?string} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the error_code field or the default value if not set.
+ * @return {string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getErrorCodeOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the error_code field.
+ * @param {string} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.setErrorCode = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the error_code field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.hasErrorCode = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the error_code field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.errorCodeCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the error_code field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.clearErrorCode = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Gets the value of the brix_error_code field.
+ * @return {?string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getBrixErrorCode = function() {
+  return /** @type {?string} */ (this.get$Value(5));
+};
+
+
+/**
+ * Gets the value of the brix_error_code field or the default value if not set.
+ * @return {string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getBrixErrorCodeOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(5));
+};
+
+
+/**
+ * Sets the value of the brix_error_code field.
+ * @param {string} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.setBrixErrorCode = function(value) {
+  this.set$Value(5, value);
+};
+
+
+/**
+ * @return {boolean} Whether the brix_error_code field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.hasBrixErrorCode = function() {
+  return this.has$Value(5);
+};
+
+
+/**
+ * @return {number} The number of values in the brix_error_code field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.brixErrorCodeCount = function() {
+  return this.count$Values(5);
+};
+
+
+/**
+ * Clears the values in the brix_error_code field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.clearBrixErrorCode = function() {
+  this.clear$Field(5);
+};
+
+
+/**
+ * Gets the value of the collaborator_joined_event field.
+ * @return {?protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getCollaboratorJoinedEvent = function() {
+  return /** @type {?protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined} */ (this.get$Value(6));
+};
+
+
+/**
+ * Gets the value of the collaborator_joined_event field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getCollaboratorJoinedEventOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined} */ (this.get$ValueOrDefault(6));
+};
+
+
+/**
+ * Sets the value of the collaborator_joined_event field.
+ * @param {!protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.setCollaboratorJoinedEvent = function(value) {
+  this.set$Value(6, value);
+};
+
+
+/**
+ * @return {boolean} Whether the collaborator_joined_event field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.hasCollaboratorJoinedEvent = function() {
+  return this.has$Value(6);
+};
+
+
+/**
+ * @return {number} The number of values in the collaborator_joined_event field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.collaboratorJoinedEventCount = function() {
+  return this.count$Values(6);
+};
+
+
+/**
+ * Clears the values in the collaborator_joined_event field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.clearCollaboratorJoinedEvent = function() {
+  this.clear$Field(6);
+};
+
+
+/**
+ * Gets the value of the document_state field.
+ * @return {?protos.research.ink.InkEvent.DocumentEvent.DocumentState} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getDocumentState = function() {
+  return /** @type {?protos.research.ink.InkEvent.DocumentEvent.DocumentState} */ (this.get$Value(7));
+};
+
+
+/**
+ * Gets the value of the document_state field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.DocumentEvent.DocumentState} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getDocumentStateOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.DocumentEvent.DocumentState} */ (this.get$ValueOrDefault(7));
+};
+
+
+/**
+ * Sets the value of the document_state field.
+ * @param {!protos.research.ink.InkEvent.DocumentEvent.DocumentState} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.setDocumentState = function(value) {
+  this.set$Value(7, value);
+};
+
+
+/**
+ * @return {boolean} Whether the document_state field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.hasDocumentState = function() {
+  return this.has$Value(7);
+};
+
+
+/**
+ * @return {number} The number of values in the document_state field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.documentStateCount = function() {
+  return this.count$Values(7);
+};
+
+
+/**
+ * Clears the values in the document_state field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.prototype.clearDocumentState = function() {
+  this.clear$Field(7);
+};
+
+
+/**
+ * Enumeration DocumentEventType.
+ * @enum {number}
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentEventType = {
+  UNKNOWN_DOCUMENT_EVENT: 0,
+  CREATED: 1,
+  OPENED: 2,
+  OPEN_FAILED: 3,
+  KICKED_USER_OUT: 4,
+  OPEN_CANCELLED: 5,
+  BRIX_DOCUMENT_CONNECT: 6,
+  UNDO: 7,
+  REDO: 8,
+  COLLABORATOR_JOINED: 9,
+  SEND: 10,
+  ABANDON: 11,
+  EXTERNAL_SHARE: 12
+};
+
+
+
+/**
+ * Message OpenedEvent.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(protos.research.ink.InkEvent.DocumentEvent.OpenedEvent, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!protos.research.ink.InkEvent.DocumentEvent.OpenedEvent} The cloned message.
+ * @override
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.clone;
+
+
+/**
+ * Gets the value of the millis_until_first_byte_loaded field.
+ * @return {?string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.getMillisUntilFirstByteLoaded = function() {
+  return /** @type {?string} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the millis_until_first_byte_loaded field or the default value if not set.
+ * @return {string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.getMillisUntilFirstByteLoadedOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the millis_until_first_byte_loaded field.
+ * @param {string} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.setMillisUntilFirstByteLoaded = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the millis_until_first_byte_loaded field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.hasMillisUntilFirstByteLoaded = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the millis_until_first_byte_loaded field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.millisUntilFirstByteLoadedCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the millis_until_first_byte_loaded field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.clearMillisUntilFirstByteLoaded = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the millis_until_editable field.
+ * @return {?string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.getMillisUntilEditable = function() {
+  return /** @type {?string} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the millis_until_editable field or the default value if not set.
+ * @return {string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.getMillisUntilEditableOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the millis_until_editable field.
+ * @param {string} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.setMillisUntilEditable = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the millis_until_editable field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.hasMillisUntilEditable = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the millis_until_editable field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.millisUntilEditableCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the millis_until_editable field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.clearMillisUntilEditable = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the missing_document_bounds field.
+ * @return {?boolean} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.getMissingDocumentBounds = function() {
+  return /** @type {?boolean} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the missing_document_bounds field or the default value if not set.
+ * @return {boolean} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.getMissingDocumentBoundsOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the missing_document_bounds field.
+ * @param {boolean} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.setMissingDocumentBounds = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the missing_document_bounds field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.hasMissingDocumentBounds = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the missing_document_bounds field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.missingDocumentBoundsCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the missing_document_bounds field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.clearMissingDocumentBounds = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the was_opened_by_cosmoid field.
+ * @return {?boolean} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.getWasOpenedByCosmoid = function() {
+  return /** @type {?boolean} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the was_opened_by_cosmoid field or the default value if not set.
+ * @return {boolean} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.getWasOpenedByCosmoidOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the was_opened_by_cosmoid field.
+ * @param {boolean} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.setWasOpenedByCosmoid = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the was_opened_by_cosmoid field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.hasWasOpenedByCosmoid = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the was_opened_by_cosmoid field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.wasOpenedByCosmoidCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the was_opened_by_cosmoid field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.clearWasOpenedByCosmoid = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Gets the value of the active_users field.
+ * @return {?string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.getActiveUsers = function() {
+  return /** @type {?string} */ (this.get$Value(5));
+};
+
+
+/**
+ * Gets the value of the active_users field or the default value if not set.
+ * @return {string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.getActiveUsersOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(5));
+};
+
+
+/**
+ * Sets the value of the active_users field.
+ * @param {string} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.setActiveUsers = function(value) {
+  this.set$Value(5, value);
+};
+
+
+/**
+ * @return {boolean} Whether the active_users field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.hasActiveUsers = function() {
+  return this.has$Value(5);
+};
+
+
+/**
+ * @return {number} The number of values in the active_users field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.activeUsersCount = function() {
+  return this.count$Values(5);
+};
+
+
+/**
+ * Clears the values in the active_users field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.clearActiveUsers = function() {
+  this.clear$Field(5);
+};
+
+
+
+/**
+ * Message OpenCancelledEvent.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent} The cloned message.
+ * @override
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent.prototype.clone;
+
+
+/**
+ * Gets the value of the time_until_cancelled field.
+ * @return {?string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent.prototype.getTimeUntilCancelled = function() {
+  return /** @type {?string} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the time_until_cancelled field or the default value if not set.
+ * @return {string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent.prototype.getTimeUntilCancelledOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the time_until_cancelled field.
+ * @param {string} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent.prototype.setTimeUntilCancelled = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the time_until_cancelled field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent.prototype.hasTimeUntilCancelled = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the time_until_cancelled field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent.prototype.timeUntilCancelledCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the time_until_cancelled field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent.prototype.clearTimeUntilCancelled = function() {
+  this.clear$Field(1);
+};
+
+
+
+/**
+ * Message CollaboratorJoined.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined} The cloned message.
+ * @override
+ */
+protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined.prototype.clone;
+
+
+/**
+ * Gets the value of the is_me field.
+ * @return {?boolean} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined.prototype.getIsMe = function() {
+  return /** @type {?boolean} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the is_me field or the default value if not set.
+ * @return {boolean} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined.prototype.getIsMeOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the is_me field.
+ * @param {boolean} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined.prototype.setIsMe = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the is_me field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined.prototype.hasIsMe = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the is_me field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined.prototype.isMeCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the is_me field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined.prototype.clearIsMe = function() {
+  this.clear$Field(1);
+};
+
+
+
+/**
+ * Message DocumentState.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(protos.research.ink.InkEvent.DocumentEvent.DocumentState, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!protos.research.ink.InkEvent.DocumentEvent.DocumentState} The cloned message.
+ * @override
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.clone;
+
+
+/**
+ * Gets the value of the stroke_count field.
+ * @return {?string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.getStrokeCount = function() {
+  return /** @type {?string} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the stroke_count field or the default value if not set.
+ * @return {string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.getStrokeCountOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the stroke_count field.
+ * @param {string} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.setStrokeCount = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the stroke_count field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.hasStrokeCount = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the stroke_count field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.strokeCountCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the stroke_count field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.clearStrokeCount = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the text_field field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.getTextField = function(index) {
+  return /** @type {?protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField} */ (this.get$Value(2, index));
+};
+
+
+/**
+ * Gets the value of the text_field field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.getTextFieldOrDefault = function(index) {
+  return /** @type {!protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField} */ (this.get$ValueOrDefault(2, index));
+};
+
+
+/**
+ * Adds a value to the text_field field.
+ * @param {!protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField} value The value to add.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.addTextField = function(value) {
+  this.add$Value(2, value);
+};
+
+
+/**
+ * Returns the array of values in the text_field field.
+ * @return {!Array<!protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField>} The values in the field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.textFieldArray = function() {
+  return /** @type {!Array<!protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField>} */ (this.array$Values(2));
+};
+
+
+/**
+ * @return {boolean} Whether the text_field field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.hasTextField = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the text_field field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.textFieldCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the text_field field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.clearTextField = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the sticker_count field.
+ * @return {?string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.getStickerCount = function() {
+  return /** @type {?string} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the sticker_count field or the default value if not set.
+ * @return {string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.getStickerCountOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the sticker_count field.
+ * @param {string} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.setStickerCount = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the sticker_count field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.hasStickerCount = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the sticker_count field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.stickerCountCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the sticker_count field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.clearStickerCount = function() {
+  this.clear$Field(3);
+};
+
+
+
+/**
+ * Message TextField.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField} The cloned message.
+ * @override
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.clone;
+
+
+/**
+ * Gets the value of the character_count field.
+ * @return {?string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.getCharacterCount = function() {
+  return /** @type {?string} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the character_count field or the default value if not set.
+ * @return {string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.getCharacterCountOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the character_count field.
+ * @param {string} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.setCharacterCount = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the character_count field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.hasCharacterCount = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the character_count field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.characterCountCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the character_count field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.clearCharacterCount = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the line_count field.
+ * @return {?string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.getLineCount = function() {
+  return /** @type {?string} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the line_count field or the default value if not set.
+ * @return {string} The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.getLineCountOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the line_count field.
+ * @param {string} value The value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.setLineCount = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the line_count field has a value.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.hasLineCount = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the line_count field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.lineCountCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the line_count field.
+ */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.clearLineCount = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message ToolbarEvent.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+protos.research.ink.InkEvent.ToolbarEvent = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(protos.research.ink.InkEvent.ToolbarEvent, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+protos.research.ink.InkEvent.ToolbarEvent.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!protos.research.ink.InkEvent.ToolbarEvent} The cloned message.
+ * @override
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.clone;
+
+
+/**
+ * Gets the value of the tool_event_type field.
+ * @return {?protos.research.ink.InkEvent.ToolbarEvent.ToolEventType} The value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.getToolEventType = function() {
+  return /** @type {?protos.research.ink.InkEvent.ToolbarEvent.ToolEventType} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the tool_event_type field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.ToolbarEvent.ToolEventType} The value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.getToolEventTypeOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.ToolbarEvent.ToolEventType} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the tool_event_type field.
+ * @param {!protos.research.ink.InkEvent.ToolbarEvent.ToolEventType} value The value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.setToolEventType = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the tool_event_type field has a value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.hasToolEventType = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the tool_event_type field.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.toolEventTypeCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the tool_event_type field.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.clearToolEventType = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the tool_type field.
+ * @return {?protos.research.ink.InkEvent.ToolbarEvent.ToolType} The value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.getToolType = function() {
+  return /** @type {?protos.research.ink.InkEvent.ToolbarEvent.ToolType} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the tool_type field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.ToolbarEvent.ToolType} The value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.getToolTypeOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.ToolbarEvent.ToolType} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the tool_type field.
+ * @param {!protos.research.ink.InkEvent.ToolbarEvent.ToolType} value The value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.setToolType = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the tool_type field has a value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.hasToolType = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the tool_type field.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.toolTypeCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the tool_type field.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.clearToolType = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the expand_method field.
+ * @return {?protos.research.ink.InkEvent.ToolbarEvent.ExpandMethod} The value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.getExpandMethod = function() {
+  return /** @type {?protos.research.ink.InkEvent.ToolbarEvent.ExpandMethod} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the expand_method field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.ToolbarEvent.ExpandMethod} The value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.getExpandMethodOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.ToolbarEvent.ExpandMethod} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the expand_method field.
+ * @param {!protos.research.ink.InkEvent.ToolbarEvent.ExpandMethod} value The value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.setExpandMethod = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the expand_method field has a value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.hasExpandMethod = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the expand_method field.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.expandMethodCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the expand_method field.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.clearExpandMethod = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the color field.
+ * @return {?number} The value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.getColor = function() {
+  return /** @type {?number} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the color field or the default value if not set.
+ * @return {number} The value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.getColorOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the color field.
+ * @param {number} value The value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.setColor = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the color field has a value.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.hasColor = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the color field.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.colorCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the color field.
+ */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.clearColor = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Enumeration ToolEventType.
+ * @enum {number}
+ */
+protos.research.ink.InkEvent.ToolbarEvent.ToolEventType = {
+  UNKNOWN_TOOL_EVENT: 0,
+  TOOLBAR_EXPANDED: 1,
+  TOOLBAR_EXTRA_COLORS_EXPANDED: 2,
+  TOOLBAR_CONTRACTED_BY_USER: 3,
+  TOOL_TYPE_CHANGED: 4,
+  TOOL_COLOR_SELECTED: 5,
+  TOOL_SIZE_SELECTED: 6,
+  CLEAR_CANVAS: 7,
+  SELECT_NONE: 8
+};
+
+
+/**
+ * Enumeration ToolType.
+ * @enum {number}
+ */
+protos.research.ink.InkEvent.ToolbarEvent.ToolType = {
+  UNKNOWN_TOOL_TYPE: 0,
+  EDIT_TOOL: 1,
+  CALLIGRAPHY: 2,
+  MARKER: 3,
+  HIGHLIGHTER: 4,
+  MAGIC_ERASER: 5
+};
+
+
+/**
+ * Enumeration ExpandMethod.
+ * @enum {number}
+ */
+protos.research.ink.InkEvent.ToolbarEvent.ExpandMethod = {
+  UNKNOWN_EXPAND_METHOD: 0,
+  SECOND_TAP: 1,
+  DRAG: 2,
+  EXPAND_BUTTON_TAP: 3
+};
+
+
+
+/**
+ * Message EngineEvent.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+protos.research.ink.InkEvent.EngineEvent = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(protos.research.ink.InkEvent.EngineEvent, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+protos.research.ink.InkEvent.EngineEvent.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!protos.research.ink.InkEvent.EngineEvent} The cloned message.
+ * @override
+ */
+protos.research.ink.InkEvent.EngineEvent.prototype.clone;
+
+
+/**
+ * Gets the value of the engine_event_type field.
+ * @return {?protos.research.ink.InkEvent.EngineEvent.EngineEventType} The value.
+ */
+protos.research.ink.InkEvent.EngineEvent.prototype.getEngineEventType = function() {
+  return /** @type {?protos.research.ink.InkEvent.EngineEvent.EngineEventType} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the engine_event_type field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.EngineEvent.EngineEventType} The value.
+ */
+protos.research.ink.InkEvent.EngineEvent.prototype.getEngineEventTypeOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.EngineEvent.EngineEventType} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the engine_event_type field.
+ * @param {!protos.research.ink.InkEvent.EngineEvent.EngineEventType} value The value.
+ */
+protos.research.ink.InkEvent.EngineEvent.prototype.setEngineEventType = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the engine_event_type field has a value.
+ */
+protos.research.ink.InkEvent.EngineEvent.prototype.hasEngineEventType = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the engine_event_type field.
+ */
+protos.research.ink.InkEvent.EngineEvent.prototype.engineEventTypeCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the engine_event_type field.
+ */
+protos.research.ink.InkEvent.EngineEvent.prototype.clearEngineEventType = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the error_code field.
+ * @return {?string} The value.
+ */
+protos.research.ink.InkEvent.EngineEvent.prototype.getErrorCode = function() {
+  return /** @type {?string} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the error_code field or the default value if not set.
+ * @return {string} The value.
+ */
+protos.research.ink.InkEvent.EngineEvent.prototype.getErrorCodeOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the error_code field.
+ * @param {string} value The value.
+ */
+protos.research.ink.InkEvent.EngineEvent.prototype.setErrorCode = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the error_code field has a value.
+ */
+protos.research.ink.InkEvent.EngineEvent.prototype.hasErrorCode = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the error_code field.
+ */
+protos.research.ink.InkEvent.EngineEvent.prototype.errorCodeCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the error_code field.
+ */
+protos.research.ink.InkEvent.EngineEvent.prototype.clearErrorCode = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Enumeration EngineEventType.
+ * @enum {number}
+ */
+protos.research.ink.InkEvent.EngineEvent.EngineEventType = {
+  UNKNOWN_ENGINE_EVENT: 0,
+  LOST_GL_CONTEXT: 1,
+  RAISED_FATAL_EXCEPTION: 2
+};
+
+
+
+/**
+ * Message GmsEvent.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+protos.research.ink.InkEvent.GmsEvent = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(protos.research.ink.InkEvent.GmsEvent, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+protos.research.ink.InkEvent.GmsEvent.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!protos.research.ink.InkEvent.GmsEvent} The cloned message.
+ * @override
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.clone;
+
+
+/**
+ * Gets the value of the gms_event_type field.
+ * @return {?protos.research.ink.InkEvent.GmsEvent.GmsEventType} The value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.getGmsEventType = function() {
+  return /** @type {?protos.research.ink.InkEvent.GmsEvent.GmsEventType} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the gms_event_type field or the default value if not set.
+ * @return {!protos.research.ink.InkEvent.GmsEvent.GmsEventType} The value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.getGmsEventTypeOrDefault = function() {
+  return /** @type {!protos.research.ink.InkEvent.GmsEvent.GmsEventType} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the gms_event_type field.
+ * @param {!protos.research.ink.InkEvent.GmsEvent.GmsEventType} value The value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.setGmsEventType = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the gms_event_type field has a value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.hasGmsEventType = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the gms_event_type field.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.gmsEventTypeCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the gms_event_type field.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.clearGmsEventType = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the time_since_connect_start field.
+ * @return {?string} The value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.getTimeSinceConnectStart = function() {
+  return /** @type {?string} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the time_since_connect_start field or the default value if not set.
+ * @return {string} The value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.getTimeSinceConnectStartOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the time_since_connect_start field.
+ * @param {string} value The value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.setTimeSinceConnectStart = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the time_since_connect_start field has a value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.hasTimeSinceConnectStart = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the time_since_connect_start field.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.timeSinceConnectStartCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the time_since_connect_start field.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.clearTimeSinceConnectStart = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the failure_has_resolution field.
+ * @return {?boolean} The value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.getFailureHasResolution = function() {
+  return /** @type {?boolean} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the failure_has_resolution field or the default value if not set.
+ * @return {boolean} The value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.getFailureHasResolutionOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the failure_has_resolution field.
+ * @param {boolean} value The value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.setFailureHasResolution = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the failure_has_resolution field has a value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.hasFailureHasResolution = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the failure_has_resolution field.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.failureHasResolutionCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the failure_has_resolution field.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.clearFailureHasResolution = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the gms_error_code field.
+ * @return {?string} The value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.getGmsErrorCode = function() {
+  return /** @type {?string} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the gms_error_code field or the default value if not set.
+ * @return {string} The value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.getGmsErrorCodeOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the gms_error_code field.
+ * @param {string} value The value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.setGmsErrorCode = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the gms_error_code field has a value.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.hasGmsErrorCode = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the gms_error_code field.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.gmsErrorCodeCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the gms_error_code field.
+ */
+protos.research.ink.InkEvent.GmsEvent.prototype.clearGmsErrorCode = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Enumeration GmsEventType.
+ * @enum {number}
+ */
+protos.research.ink.InkEvent.GmsEvent.GmsEventType = {
+  CONNECT_SUCCESS: 0,
+  CONNECT_FAILED: 1,
+  STOPPED_WHEN_PENDING_CONNECT: 2
+};
+
+
+/** @override */
+protos.research.ink.InkEvent.prototype.getDescriptor = function() {
+  var descriptor = protos.research.ink.InkEvent.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'InkEvent',
+        fullName: 'logs.proto.research.ink.InkEvent'
+      },
+      1: {
+        name: 'host',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: protos.research.ink.InkEvent.Host.UNKNOWN_HOST,
+        type: protos.research.ink.InkEvent.Host
+      },
+      2: {
+        name: 'event_type',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: protos.research.ink.InkEvent.EventType.UNKNOWN_TYPE,
+        type: protos.research.ink.InkEvent.EventType
+      },
+      3: {
+        name: 'document_event',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: protos.research.ink.InkEvent.DocumentEvent
+      },
+      4: {
+        name: 'toolbar_event',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: protos.research.ink.InkEvent.ToolbarEvent
+      },
+      5: {
+        name: 'engine_event',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: protos.research.ink.InkEvent.EngineEvent
+      },
+      6: {
+        name: 'gms_event',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: protos.research.ink.InkEvent.GmsEvent
+      }
+    };
+    protos.research.ink.InkEvent.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             protos.research.ink.InkEvent, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+protos.research.ink.InkEvent.getDescriptor =
+    protos.research.ink.InkEvent.prototype.getDescriptor;
+
+
+/** @override */
+protos.research.ink.InkEvent.DocumentEvent.prototype.getDescriptor = function() {
+  var descriptor = protos.research.ink.InkEvent.DocumentEvent.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'DocumentEvent',
+        containingType: protos.research.ink.InkEvent,
+        fullName: 'logs.proto.research.ink.InkEvent.DocumentEvent'
+      },
+      1: {
+        name: 'event_type',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: protos.research.ink.InkEvent.DocumentEvent.DocumentEventType.UNKNOWN_DOCUMENT_EVENT,
+        type: protos.research.ink.InkEvent.DocumentEvent.DocumentEventType
+      },
+      2: {
+        name: 'opened_event',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: protos.research.ink.InkEvent.DocumentEvent.OpenedEvent
+      },
+      3: {
+        name: 'open_cancelled_event',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent
+      },
+      4: {
+        name: 'error_code',
+        fieldType: goog.proto2.Message.FieldType.INT64,
+        type: String
+      },
+      5: {
+        name: 'brix_error_code',
+        fieldType: goog.proto2.Message.FieldType.INT64,
+        type: String
+      },
+      6: {
+        name: 'collaborator_joined_event',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined
+      },
+      7: {
+        name: 'document_state',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: protos.research.ink.InkEvent.DocumentEvent.DocumentState
+      }
+    };
+    protos.research.ink.InkEvent.DocumentEvent.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             protos.research.ink.InkEvent.DocumentEvent, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+protos.research.ink.InkEvent.DocumentEvent.getDescriptor =
+    protos.research.ink.InkEvent.DocumentEvent.prototype.getDescriptor;
+
+
+/** @override */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.getDescriptor = function() {
+  var descriptor = protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'OpenedEvent',
+        containingType: protos.research.ink.InkEvent.DocumentEvent,
+        fullName: 'logs.proto.research.ink.InkEvent.DocumentEvent.OpenedEvent'
+      },
+      1: {
+        name: 'millis_until_first_byte_loaded',
+        fieldType: goog.proto2.Message.FieldType.INT64,
+        type: String
+      },
+      2: {
+        name: 'millis_until_editable',
+        fieldType: goog.proto2.Message.FieldType.INT64,
+        type: String
+      },
+      3: {
+        name: 'missing_document_bounds',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        type: Boolean
+      },
+      4: {
+        name: 'was_opened_by_cosmoid',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        type: Boolean
+      },
+      5: {
+        name: 'active_users',
+        fieldType: goog.proto2.Message.FieldType.INT64,
+        type: String
+      }
+    };
+    protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             protos.research.ink.InkEvent.DocumentEvent.OpenedEvent, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.getDescriptor =
+    protos.research.ink.InkEvent.DocumentEvent.OpenedEvent.prototype.getDescriptor;
+
+
+/** @override */
+protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent.prototype.getDescriptor = function() {
+  var descriptor = protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'OpenCancelledEvent',
+        containingType: protos.research.ink.InkEvent.DocumentEvent,
+        fullName: 'logs.proto.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent'
+      },
+      1: {
+        name: 'time_until_cancelled',
+        fieldType: goog.proto2.Message.FieldType.INT64,
+        type: String
+      }
+    };
+    protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent.getDescriptor =
+    protos.research.ink.InkEvent.DocumentEvent.OpenCancelledEvent.prototype.getDescriptor;
+
+
+/** @override */
+protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined.prototype.getDescriptor = function() {
+  var descriptor = protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'CollaboratorJoined',
+        containingType: protos.research.ink.InkEvent.DocumentEvent,
+        fullName: 'logs.proto.research.ink.InkEvent.DocumentEvent.CollaboratorJoined'
+      },
+      1: {
+        name: 'is_me',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        type: Boolean
+      }
+    };
+    protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined.getDescriptor =
+    protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined.prototype.getDescriptor;
+
+
+/** @override */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.getDescriptor = function() {
+  var descriptor = protos.research.ink.InkEvent.DocumentEvent.DocumentState.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'DocumentState',
+        containingType: protos.research.ink.InkEvent.DocumentEvent,
+        fullName: 'logs.proto.research.ink.InkEvent.DocumentEvent.DocumentState'
+      },
+      1: {
+        name: 'stroke_count',
+        fieldType: goog.proto2.Message.FieldType.INT64,
+        type: String
+      },
+      2: {
+        name: 'text_field',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField
+      },
+      3: {
+        name: 'sticker_count',
+        fieldType: goog.proto2.Message.FieldType.INT64,
+        type: String
+      }
+    };
+    protos.research.ink.InkEvent.DocumentEvent.DocumentState.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             protos.research.ink.InkEvent.DocumentEvent.DocumentState, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.getDescriptor =
+    protos.research.ink.InkEvent.DocumentEvent.DocumentState.prototype.getDescriptor;
+
+
+/** @override */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.getDescriptor = function() {
+  var descriptor = protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'TextField',
+        containingType: protos.research.ink.InkEvent.DocumentEvent.DocumentState,
+        fullName: 'logs.proto.research.ink.InkEvent.DocumentEvent.DocumentState.TextField'
+      },
+      1: {
+        name: 'character_count',
+        fieldType: goog.proto2.Message.FieldType.INT64,
+        type: String
+      },
+      2: {
+        name: 'line_count',
+        fieldType: goog.proto2.Message.FieldType.INT64,
+        type: String
+      }
+    };
+    protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.getDescriptor =
+    protos.research.ink.InkEvent.DocumentEvent.DocumentState.TextField.prototype.getDescriptor;
+
+
+/** @override */
+protos.research.ink.InkEvent.ToolbarEvent.prototype.getDescriptor = function() {
+  var descriptor = protos.research.ink.InkEvent.ToolbarEvent.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ToolbarEvent',
+        containingType: protos.research.ink.InkEvent,
+        fullName: 'logs.proto.research.ink.InkEvent.ToolbarEvent'
+      },
+      1: {
+        name: 'tool_event_type',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: protos.research.ink.InkEvent.ToolbarEvent.ToolEventType.UNKNOWN_TOOL_EVENT,
+        type: protos.research.ink.InkEvent.ToolbarEvent.ToolEventType
+      },
+      2: {
+        name: 'tool_type',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: protos.research.ink.InkEvent.ToolbarEvent.ToolType.UNKNOWN_TOOL_TYPE,
+        type: protos.research.ink.InkEvent.ToolbarEvent.ToolType
+      },
+      3: {
+        name: 'expand_method',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: protos.research.ink.InkEvent.ToolbarEvent.ExpandMethod.UNKNOWN_EXPAND_METHOD,
+        type: protos.research.ink.InkEvent.ToolbarEvent.ExpandMethod
+      },
+      4: {
+        name: 'color',
+        fieldType: goog.proto2.Message.FieldType.INT32,
+        type: Number
+      }
+    };
+    protos.research.ink.InkEvent.ToolbarEvent.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             protos.research.ink.InkEvent.ToolbarEvent, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+protos.research.ink.InkEvent.ToolbarEvent.getDescriptor =
+    protos.research.ink.InkEvent.ToolbarEvent.prototype.getDescriptor;
+
+
+/** @override */
+protos.research.ink.InkEvent.EngineEvent.prototype.getDescriptor = function() {
+  var descriptor = protos.research.ink.InkEvent.EngineEvent.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'EngineEvent',
+        containingType: protos.research.ink.InkEvent,
+        fullName: 'logs.proto.research.ink.InkEvent.EngineEvent'
+      },
+      1: {
+        name: 'engine_event_type',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: protos.research.ink.InkEvent.EngineEvent.EngineEventType.UNKNOWN_ENGINE_EVENT,
+        type: protos.research.ink.InkEvent.EngineEvent.EngineEventType
+      },
+      2: {
+        name: 'error_code',
+        fieldType: goog.proto2.Message.FieldType.INT64,
+        type: String
+      }
+    };
+    protos.research.ink.InkEvent.EngineEvent.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             protos.research.ink.InkEvent.EngineEvent, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+protos.research.ink.InkEvent.EngineEvent.getDescriptor =
+    protos.research.ink.InkEvent.EngineEvent.prototype.getDescriptor;
+
+
+/** @override */
+protos.research.ink.InkEvent.GmsEvent.prototype.getDescriptor = function() {
+  var descriptor = protos.research.ink.InkEvent.GmsEvent.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'GmsEvent',
+        containingType: protos.research.ink.InkEvent,
+        fullName: 'logs.proto.research.ink.InkEvent.GmsEvent'
+      },
+      1: {
+        name: 'gms_event_type',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: protos.research.ink.InkEvent.GmsEvent.GmsEventType.CONNECT_SUCCESS,
+        type: protos.research.ink.InkEvent.GmsEvent.GmsEventType
+      },
+      2: {
+        name: 'time_since_connect_start',
+        fieldType: goog.proto2.Message.FieldType.INT64,
+        type: String
+      },
+      3: {
+        name: 'failure_has_resolution',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        type: Boolean
+      },
+      4: {
+        name: 'gms_error_code',
+        fieldType: goog.proto2.Message.FieldType.INT64,
+        type: String
+      }
+    };
+    protos.research.ink.InkEvent.GmsEvent.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             protos.research.ink.InkEvent.GmsEvent, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+protos.research.ink.InkEvent.GmsEvent.getDescriptor =
+    protos.research.ink.InkEvent.GmsEvent.prototype.getDescriptor;
diff --git a/third_party/ink/ink_scripts.js b/third_party/ink/ink_scripts.js
new file mode 100644
index 0000000..b056e6d6
--- /dev/null
+++ b/third_party/ink/ink_scripts.js
@@ -0,0 +1,118 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// The include directives are put into Javascript-style comments to prevent
+// parsing errors in non-flattened mode. The flattener still sees them.
+// Note that this makes the flattener to comment out the first line of the
+// included file but that's all right since any javascript file should start
+// with a copyright comment anyway.
+
+// <include src="ink/web/js/main.soy.js">
+// <include src="ink/web/js/cursor_updater.js">
+// <include src="ink/web/js/canvas_manager/canvas_manager.js">
+// <include src="ink/web/js/embed/embed_component.js">
+// <include src="ink/web/js/embed/events.js">
+// <include src="ink/web/js/embed/embed.js">
+// <include src="template/soy/soyutils_usegoog.js">
+// <include src="ink_event.pb.js">
+// <include src="sketchology/public/nacl/embed.soy.js">
+// <include src="sketchology/public/nacl/sketchology_engine_wrapper.js">
+// <include src="sketchology/public/js/common/color.js">
+// <include src="sketchology/public/js/common/model.js">
+// <include src="sketchology/public/js/common/undo_state_change_event.js">
+// <include src="sketchology/public/js/common/proto_serializer.js">
+// <include src="sketchology/public/js/common/util.js">
+// <include src="sketchology/public/js/common/element_listener.js">
+// <include src="sketchology/public/js/common/brush_model.js">
+// <include src="sketchology/proto/rect_bounds.pb.js">
+// <include src="sketchology/proto/elements.pb.js">
+// <include src="sketchology/proto/animations.pb.js">
+// <include src="sketchology/proto/sengine.pb.js">
+// <include src="sketchology/proto/document.pb.js">
+// <include src="wireserializer.js">
+// <include src="closure/functions/functions.js">
+// <include src="closure/base.js">
+// <include src="closure/fs/url.js">
+// <include src="closure/uri/utils.js">
+// <include src="closure/uri/uri.js">
+// <include src="closure/style/style.js">
+// <include src="closure/crypt/crypt.js">
+// <include src="closure/crypt/base64.js">
+// <include src="closure/ui/component.js">
+// <include src="closure/ui/idgenerator.js">
+// <include src="closure/labs/useragent/platform.js">
+// <include src="closure/labs/useragent/browser.js">
+// <include src="closure/labs/useragent/util.js">
+// <include src="closure/labs/useragent/engine.js">
+// <include src="closure/string/const.js">
+// <include src="closure/string/string.js">
+// <include src="closure/string/typedstring.js">
+// <include src="closure/structs/set.js">
+// <include src="closure/structs/inversionmap.js">
+// <include src="closure/structs/collection.js">
+// <include src="closure/structs/structs.js">
+// <include src="closure/structs/map.js">
+// <include src="closure/proto2/serializer.js">
+// <include src="closure/proto2/objectserializer.js">
+// <include src="closure/proto2/message.js">
+// <include src="closure/proto2/fielddescriptor.js">
+// <include src="closure/proto2/descriptor.js">
+// <include src="closure/html/safehtml.js">
+// <include src="closure/html/safescript.js">
+// <include src="closure/html/safestyle.js">
+// <include src="closure/html/uncheckedconversions.js">
+// <include src="closure/html/safestylesheet.js">
+// <include src="closure/html/legacyconversions.js">
+// <include src="closure/html/trustedresourceurl.js">
+// <include src="closure/html/safeurl.js">
+// <include src="closure/math/long.js">
+// <include src="closure/math/irect.js">
+// <include src="closure/math/size.js">
+// <include src="closure/math/math.js">
+// <include src="closure/math/rect.js">
+// <include src="closure/math/coordinate.js">
+// <include src="closure/math/box.js">
+// <include src="closure/object/object.js">
+// <include src="closure/reflect/reflect.js">
+// <include src="closure/useragent/useragent.js">
+// <include src="closure/useragent/product.js">
+// <include src="closure/i18n/uchar.js">
+// <include src="closure/i18n/bidi.js">
+// <include src="closure/i18n/graphemebreak.js">
+// <include src="closure/i18n/bidiformatter.js">
+// <include src="closure/dom/tagname.js">
+// <include src="closure/dom/classlist.js">
+// <include src="closure/dom/asserts.js">
+// <include src="closure/dom/nodetype.js">
+// <include src="closure/dom/dom.js">
+// <include src="closure/dom/tags.js">
+// <include src="closure/dom/safe.js">
+// <include src="closure/dom/browserfeature.js">
+// <include src="closure/dom/htmlelement.js">
+// <include src="closure/dom/vendor.js">
+// <include src="closure/soy/soy.js">
+// <include src="closure/soy/data.js">
+// <include src="closure/format/format.js">
+// <include src="closure/asserts/asserts.js">
+// <include src="closure/array/array.js">
+// <include src="closure/iter/iter.js">
+// <include src="closure/events/eventid.js">
+// <include src="closure/events/event.js">
+// <include src="closure/events/listener.js">
+// <include src="closure/events/listenable.js">
+// <include src="closure/events/browserevent.js">
+// <include src="closure/events/events.js">
+// <include src="closure/events/browserfeature.js">
+// <include src="closure/events/eventtarget.js">
+// <include src="closure/events/eventhandler.js">
+// <include src="closure/events/listenermap.js">
+// <include src="closure/events/keycodes.js">
+// <include src="closure/events/wheelevent.js">
+// <include src="closure/events/eventtype.js">
+// <include src="closure/disposable/idisposable.js">
+// <include src="closure/disposable/disposable.js">
+// <include src="closure/debug/debug.js">
+// <include src="closure/debug/errorcontext.js">
+// <include src="closure/debug/entrypointregistry.js">
+// <include src="closure/debug/error.js">
diff --git a/third_party/ink/sketchology/proto/animations.pb.js b/third_party/ink/sketchology/proto/animations.pb.js
new file mode 100644
index 0000000..da7702c
--- /dev/null
+++ b/third_party/ink/sketchology/proto/animations.pb.js
@@ -0,0 +1,984 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// Protocol Buffer 2 Copyright 2008 Google Inc.
+// All other code copyright its respective owners.
+
+/**
+ * @fileoverview Generated Protocol Buffer code for file
+ * third_party/sketchology/proto/animations.proto.
+ * Generated by //net/proto2/compiler/public:protocol_compiler.
+ * @suppress {messageConventions} 
+ */
+
+goog.provide('sketchology.proto.AnimationCurve');
+goog.provide('sketchology.proto.ColorAnimation');
+goog.provide('sketchology.proto.ScaleAnimation');
+goog.provide('sketchology.proto.ElementAnimation');
+goog.provide('sketchology.proto.CurveType');
+
+goog.require('goog.proto2.Message');
+
+
+/**
+ * Enumeration CurveType.
+ * @enum {number}
+ */
+sketchology.proto.CurveType = {
+  UNSPECIFIED_CURVE_TYPE: 0,
+  EASE_IN_OUT: 1,
+  EASE_IN: 2,
+  EASE_OUT: 3,
+  CUSTOM_CUBIC_BEZIER: 4
+};
+
+
+
+/**
+ * Message AnimationCurve.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.AnimationCurve = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.AnimationCurve, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.AnimationCurve.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.AnimationCurve} The cloned message.
+ * @override
+ */
+sketchology.proto.AnimationCurve.prototype.clone;
+
+
+/**
+ * Gets the value of the type field.
+ * @return {?sketchology.proto.CurveType} The value.
+ */
+sketchology.proto.AnimationCurve.prototype.getType = function() {
+  return /** @type {?sketchology.proto.CurveType} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the type field or the default value if not set.
+ * @return {!sketchology.proto.CurveType} The value.
+ */
+sketchology.proto.AnimationCurve.prototype.getTypeOrDefault = function() {
+  return /** @type {!sketchology.proto.CurveType} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the type field.
+ * @param {!sketchology.proto.CurveType} value The value.
+ */
+sketchology.proto.AnimationCurve.prototype.setType = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the type field has a value.
+ */
+sketchology.proto.AnimationCurve.prototype.hasType = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the type field.
+ */
+sketchology.proto.AnimationCurve.prototype.typeCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the type field.
+ */
+sketchology.proto.AnimationCurve.prototype.clearType = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the params field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?number} The value.
+ */
+sketchology.proto.AnimationCurve.prototype.getParams = function(index) {
+  return /** @type {?number} */ (this.get$Value(2, index));
+};
+
+
+/**
+ * Gets the value of the params field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {number} The value.
+ */
+sketchology.proto.AnimationCurve.prototype.getParamsOrDefault = function(index) {
+  return /** @type {number} */ (this.get$ValueOrDefault(2, index));
+};
+
+
+/**
+ * Adds a value to the params field.
+ * @param {number} value The value to add.
+ */
+sketchology.proto.AnimationCurve.prototype.addParams = function(value) {
+  this.add$Value(2, value);
+};
+
+
+/**
+ * Returns the array of values in the params field.
+ * @return {!Array<number>} The values in the field.
+ */
+sketchology.proto.AnimationCurve.prototype.paramsArray = function() {
+  return /** @type {!Array<number>} */ (this.array$Values(2));
+};
+
+
+/**
+ * @return {boolean} Whether the params field has a value.
+ */
+sketchology.proto.AnimationCurve.prototype.hasParams = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the params field.
+ */
+sketchology.proto.AnimationCurve.prototype.paramsCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the params field.
+ */
+sketchology.proto.AnimationCurve.prototype.clearParams = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message ColorAnimation.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ColorAnimation = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ColorAnimation, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ColorAnimation.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ColorAnimation} The cloned message.
+ * @override
+ */
+sketchology.proto.ColorAnimation.prototype.clone;
+
+
+/**
+ * Gets the value of the duration field.
+ * @return {?number} The value.
+ */
+sketchology.proto.ColorAnimation.prototype.getDuration = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the duration field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.ColorAnimation.prototype.getDurationOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the duration field.
+ * @param {number} value The value.
+ */
+sketchology.proto.ColorAnimation.prototype.setDuration = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the duration field has a value.
+ */
+sketchology.proto.ColorAnimation.prototype.hasDuration = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the duration field.
+ */
+sketchology.proto.ColorAnimation.prototype.durationCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the duration field.
+ */
+sketchology.proto.ColorAnimation.prototype.clearDuration = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the curve field.
+ * @return {?sketchology.proto.AnimationCurve} The value.
+ */
+sketchology.proto.ColorAnimation.prototype.getCurve = function() {
+  return /** @type {?sketchology.proto.AnimationCurve} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the curve field or the default value if not set.
+ * @return {!sketchology.proto.AnimationCurve} The value.
+ */
+sketchology.proto.ColorAnimation.prototype.getCurveOrDefault = function() {
+  return /** @type {!sketchology.proto.AnimationCurve} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the curve field.
+ * @param {!sketchology.proto.AnimationCurve} value The value.
+ */
+sketchology.proto.ColorAnimation.prototype.setCurve = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the curve field has a value.
+ */
+sketchology.proto.ColorAnimation.prototype.hasCurve = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the curve field.
+ */
+sketchology.proto.ColorAnimation.prototype.curveCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the curve field.
+ */
+sketchology.proto.ColorAnimation.prototype.clearCurve = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the rgba field.
+ * @return {?number} The value.
+ */
+sketchology.proto.ColorAnimation.prototype.getRgba = function() {
+  return /** @type {?number} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the rgba field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.ColorAnimation.prototype.getRgbaOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the rgba field.
+ * @param {number} value The value.
+ */
+sketchology.proto.ColorAnimation.prototype.setRgba = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the rgba field has a value.
+ */
+sketchology.proto.ColorAnimation.prototype.hasRgba = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the rgba field.
+ */
+sketchology.proto.ColorAnimation.prototype.rgbaCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the rgba field.
+ */
+sketchology.proto.ColorAnimation.prototype.clearRgba = function() {
+  this.clear$Field(3);
+};
+
+
+
+/**
+ * Message ScaleAnimation.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ScaleAnimation = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ScaleAnimation, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ScaleAnimation.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ScaleAnimation} The cloned message.
+ * @override
+ */
+sketchology.proto.ScaleAnimation.prototype.clone;
+
+
+/**
+ * Gets the value of the duration field.
+ * @return {?number} The value.
+ */
+sketchology.proto.ScaleAnimation.prototype.getDuration = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the duration field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.ScaleAnimation.prototype.getDurationOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the duration field.
+ * @param {number} value The value.
+ */
+sketchology.proto.ScaleAnimation.prototype.setDuration = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the duration field has a value.
+ */
+sketchology.proto.ScaleAnimation.prototype.hasDuration = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the duration field.
+ */
+sketchology.proto.ScaleAnimation.prototype.durationCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the duration field.
+ */
+sketchology.proto.ScaleAnimation.prototype.clearDuration = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the curve field.
+ * @return {?sketchology.proto.AnimationCurve} The value.
+ */
+sketchology.proto.ScaleAnimation.prototype.getCurve = function() {
+  return /** @type {?sketchology.proto.AnimationCurve} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the curve field or the default value if not set.
+ * @return {!sketchology.proto.AnimationCurve} The value.
+ */
+sketchology.proto.ScaleAnimation.prototype.getCurveOrDefault = function() {
+  return /** @type {!sketchology.proto.AnimationCurve} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the curve field.
+ * @param {!sketchology.proto.AnimationCurve} value The value.
+ */
+sketchology.proto.ScaleAnimation.prototype.setCurve = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the curve field has a value.
+ */
+sketchology.proto.ScaleAnimation.prototype.hasCurve = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the curve field.
+ */
+sketchology.proto.ScaleAnimation.prototype.curveCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the curve field.
+ */
+sketchology.proto.ScaleAnimation.prototype.clearCurve = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the scale_x field.
+ * @return {?number} The value.
+ */
+sketchology.proto.ScaleAnimation.prototype.getScaleX = function() {
+  return /** @type {?number} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the scale_x field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.ScaleAnimation.prototype.getScaleXOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the scale_x field.
+ * @param {number} value The value.
+ */
+sketchology.proto.ScaleAnimation.prototype.setScaleX = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the scale_x field has a value.
+ */
+sketchology.proto.ScaleAnimation.prototype.hasScaleX = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the scale_x field.
+ */
+sketchology.proto.ScaleAnimation.prototype.scaleXCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the scale_x field.
+ */
+sketchology.proto.ScaleAnimation.prototype.clearScaleX = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the scale_y field.
+ * @return {?number} The value.
+ */
+sketchology.proto.ScaleAnimation.prototype.getScaleY = function() {
+  return /** @type {?number} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the scale_y field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.ScaleAnimation.prototype.getScaleYOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the scale_y field.
+ * @param {number} value The value.
+ */
+sketchology.proto.ScaleAnimation.prototype.setScaleY = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the scale_y field has a value.
+ */
+sketchology.proto.ScaleAnimation.prototype.hasScaleY = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the scale_y field.
+ */
+sketchology.proto.ScaleAnimation.prototype.scaleYCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the scale_y field.
+ */
+sketchology.proto.ScaleAnimation.prototype.clearScaleY = function() {
+  this.clear$Field(4);
+};
+
+
+
+/**
+ * Message ElementAnimation.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ElementAnimation = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ElementAnimation, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ElementAnimation.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ElementAnimation} The cloned message.
+ * @override
+ */
+sketchology.proto.ElementAnimation.prototype.clone;
+
+
+/**
+ * Gets the value of the uuid field.
+ * @return {?string} The value.
+ */
+sketchology.proto.ElementAnimation.prototype.getUuid = function() {
+  return /** @type {?string} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the uuid field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.ElementAnimation.prototype.getUuidOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the uuid field.
+ * @param {string} value The value.
+ */
+sketchology.proto.ElementAnimation.prototype.setUuid = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uuid field has a value.
+ */
+sketchology.proto.ElementAnimation.prototype.hasUuid = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uuid field.
+ */
+sketchology.proto.ElementAnimation.prototype.uuidCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uuid field.
+ */
+sketchology.proto.ElementAnimation.prototype.clearUuid = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the color_animation field.
+ * @return {?sketchology.proto.ColorAnimation} The value.
+ */
+sketchology.proto.ElementAnimation.prototype.getColorAnimation = function() {
+  return /** @type {?sketchology.proto.ColorAnimation} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the color_animation field or the default value if not set.
+ * @return {!sketchology.proto.ColorAnimation} The value.
+ */
+sketchology.proto.ElementAnimation.prototype.getColorAnimationOrDefault = function() {
+  return /** @type {!sketchology.proto.ColorAnimation} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the color_animation field.
+ * @param {!sketchology.proto.ColorAnimation} value The value.
+ */
+sketchology.proto.ElementAnimation.prototype.setColorAnimation = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the color_animation field has a value.
+ */
+sketchology.proto.ElementAnimation.prototype.hasColorAnimation = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the color_animation field.
+ */
+sketchology.proto.ElementAnimation.prototype.colorAnimationCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the color_animation field.
+ */
+sketchology.proto.ElementAnimation.prototype.clearColorAnimation = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the scale_animation field.
+ * @return {?sketchology.proto.ScaleAnimation} The value.
+ */
+sketchology.proto.ElementAnimation.prototype.getScaleAnimation = function() {
+  return /** @type {?sketchology.proto.ScaleAnimation} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the scale_animation field or the default value if not set.
+ * @return {!sketchology.proto.ScaleAnimation} The value.
+ */
+sketchology.proto.ElementAnimation.prototype.getScaleAnimationOrDefault = function() {
+  return /** @type {!sketchology.proto.ScaleAnimation} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the scale_animation field.
+ * @param {!sketchology.proto.ScaleAnimation} value The value.
+ */
+sketchology.proto.ElementAnimation.prototype.setScaleAnimation = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the scale_animation field has a value.
+ */
+sketchology.proto.ElementAnimation.prototype.hasScaleAnimation = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the scale_animation field.
+ */
+sketchology.proto.ElementAnimation.prototype.scaleAnimationCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the scale_animation field.
+ */
+sketchology.proto.ElementAnimation.prototype.clearScaleAnimation = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the next field.
+ * @return {?sketchology.proto.ElementAnimation} The value.
+ */
+sketchology.proto.ElementAnimation.prototype.getNext = function() {
+  return /** @type {?sketchology.proto.ElementAnimation} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the next field or the default value if not set.
+ * @return {!sketchology.proto.ElementAnimation} The value.
+ */
+sketchology.proto.ElementAnimation.prototype.getNextOrDefault = function() {
+  return /** @type {!sketchology.proto.ElementAnimation} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the next field.
+ * @param {!sketchology.proto.ElementAnimation} value The value.
+ */
+sketchology.proto.ElementAnimation.prototype.setNext = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the next field has a value.
+ */
+sketchology.proto.ElementAnimation.prototype.hasNext = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the next field.
+ */
+sketchology.proto.ElementAnimation.prototype.nextCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the next field.
+ */
+sketchology.proto.ElementAnimation.prototype.clearNext = function() {
+  this.clear$Field(4);
+};
+
+
+/** @override */
+sketchology.proto.AnimationCurve.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.AnimationCurve.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'AnimationCurve',
+        fullName: 'sketchology.proto.AnimationCurve'
+      },
+      1: {
+        name: 'type',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: sketchology.proto.CurveType.EASE_IN_OUT,
+        type: sketchology.proto.CurveType
+      },
+      2: {
+        name: 'params',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      }
+    };
+    sketchology.proto.AnimationCurve.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.AnimationCurve, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.AnimationCurve.getDescriptor =
+    sketchology.proto.AnimationCurve.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ColorAnimation.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ColorAnimation.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ColorAnimation',
+        fullName: 'sketchology.proto.ColorAnimation'
+      },
+      1: {
+        name: 'duration',
+        fieldType: goog.proto2.Message.FieldType.DOUBLE,
+        defaultValue: 0.5,
+        type: Number
+      },
+      2: {
+        name: 'curve',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.AnimationCurve
+      },
+      3: {
+        name: 'rgba',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      }
+    };
+    sketchology.proto.ColorAnimation.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ColorAnimation, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ColorAnimation.getDescriptor =
+    sketchology.proto.ColorAnimation.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ScaleAnimation.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ScaleAnimation.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ScaleAnimation',
+        fullName: 'sketchology.proto.ScaleAnimation'
+      },
+      1: {
+        name: 'duration',
+        fieldType: goog.proto2.Message.FieldType.DOUBLE,
+        defaultValue: 0.5,
+        type: Number
+      },
+      2: {
+        name: 'curve',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.AnimationCurve
+      },
+      3: {
+        name: 'scale_x',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      4: {
+        name: 'scale_y',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      }
+    };
+    sketchology.proto.ScaleAnimation.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ScaleAnimation, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ScaleAnimation.getDescriptor =
+    sketchology.proto.ScaleAnimation.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ElementAnimation.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ElementAnimation.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ElementAnimation',
+        fullName: 'sketchology.proto.ElementAnimation'
+      },
+      1: {
+        name: 'uuid',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      2: {
+        name: 'color_animation',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ColorAnimation
+      },
+      3: {
+        name: 'scale_animation',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ScaleAnimation
+      },
+      4: {
+        name: 'next',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ElementAnimation
+      }
+    };
+    sketchology.proto.ElementAnimation.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ElementAnimation, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ElementAnimation.getDescriptor =
+    sketchology.proto.ElementAnimation.prototype.getDescriptor;
diff --git a/third_party/ink/sketchology/proto/document.pb.js b/third_party/ink/sketchology/proto/document.pb.js
new file mode 100644
index 0000000..1826d1c5
--- /dev/null
+++ b/third_party/ink/sketchology/proto/document.pb.js
@@ -0,0 +1,2831 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// Protocol Buffer 2 Copyright 2008 Google Inc.
+// All other code copyright its respective owners.
+
+/**
+ * @fileoverview Generated Protocol Buffer code for file
+ * third_party/sketchology/proto/document.proto.
+ * Generated by //net/proto2/compiler/public:protocol_compiler.
+ * @suppress {messageConventions} 
+ */
+
+goog.provide('sketchology.proto.Color');
+goog.provide('sketchology.proto.BackgroundColor');
+goog.provide('sketchology.proto.PageProperties');
+goog.provide('sketchology.proto.AddAction');
+goog.provide('sketchology.proto.RemoveAction');
+goog.provide('sketchology.proto.ClearAction');
+goog.provide('sketchology.proto.ReplaceAction');
+goog.provide('sketchology.proto.SetTransformAction');
+goog.provide('sketchology.proto.SetPageBoundsAction');
+goog.provide('sketchology.proto.StorageAction');
+goog.provide('sketchology.proto.Snapshot');
+goog.provide('sketchology.proto.MutationPacket');
+goog.provide('sketchology.proto.StorageActionState');
+goog.provide('sketchology.proto.ElementState');
+
+goog.require('goog.proto2.Message');
+goog.require('sketchology.proto.AffineTransform');
+goog.require('sketchology.proto.BackgroundImageInfo');
+goog.require('sketchology.proto.Border');
+goog.require('sketchology.proto.ElementBundle');
+goog.require('sketchology.proto.Rect');
+
+
+/**
+ * Enumeration StorageActionState.
+ * @enum {number}
+ */
+sketchology.proto.StorageActionState = {
+  APPLIED: 1,
+  UNDONE: 2
+};
+
+
+/**
+ * Enumeration ElementState.
+ * @enum {number}
+ */
+sketchology.proto.ElementState = {
+  ALIVE: 1,
+  DEAD: 2
+};
+
+
+
+/**
+ * Message Color.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.Color = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.Color, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.Color.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.Color} The cloned message.
+ * @override
+ */
+sketchology.proto.Color.prototype.clone;
+
+
+/**
+ * Gets the value of the argb field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Color.prototype.getArgb = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the argb field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Color.prototype.getArgbOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the argb field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Color.prototype.setArgb = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the argb field has a value.
+ */
+sketchology.proto.Color.prototype.hasArgb = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the argb field.
+ */
+sketchology.proto.Color.prototype.argbCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the argb field.
+ */
+sketchology.proto.Color.prototype.clearArgb = function() {
+  this.clear$Field(1);
+};
+
+
+
+/**
+ * Message BackgroundColor.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.BackgroundColor = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.BackgroundColor, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.BackgroundColor.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.BackgroundColor} The cloned message.
+ * @override
+ */
+sketchology.proto.BackgroundColor.prototype.clone;
+
+
+/**
+ * Gets the value of the rgba field.
+ * @return {?number} The value.
+ */
+sketchology.proto.BackgroundColor.prototype.getRgba = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the rgba field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.BackgroundColor.prototype.getRgbaOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the rgba field.
+ * @param {number} value The value.
+ */
+sketchology.proto.BackgroundColor.prototype.setRgba = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the rgba field has a value.
+ */
+sketchology.proto.BackgroundColor.prototype.hasRgba = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the rgba field.
+ */
+sketchology.proto.BackgroundColor.prototype.rgbaCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the rgba field.
+ */
+sketchology.proto.BackgroundColor.prototype.clearRgba = function() {
+  this.clear$Field(1);
+};
+
+
+
+/**
+ * Message PageProperties.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.PageProperties = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.PageProperties, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.PageProperties.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.PageProperties} The cloned message.
+ * @override
+ */
+sketchology.proto.PageProperties.prototype.clone;
+
+
+/**
+ * Gets the value of the background_color field.
+ * @return {?sketchology.proto.Color} The value.
+ */
+sketchology.proto.PageProperties.prototype.getBackgroundColor = function() {
+  return /** @type {?sketchology.proto.Color} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the background_color field or the default value if not set.
+ * @return {!sketchology.proto.Color} The value.
+ */
+sketchology.proto.PageProperties.prototype.getBackgroundColorOrDefault = function() {
+  return /** @type {!sketchology.proto.Color} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the background_color field.
+ * @param {!sketchology.proto.Color} value The value.
+ */
+sketchology.proto.PageProperties.prototype.setBackgroundColor = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the background_color field has a value.
+ */
+sketchology.proto.PageProperties.prototype.hasBackgroundColor = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the background_color field.
+ */
+sketchology.proto.PageProperties.prototype.backgroundColorCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the background_color field.
+ */
+sketchology.proto.PageProperties.prototype.clearBackgroundColor = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the background_image field.
+ * @return {?sketchology.proto.BackgroundImageInfo} The value.
+ */
+sketchology.proto.PageProperties.prototype.getBackgroundImage = function() {
+  return /** @type {?sketchology.proto.BackgroundImageInfo} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the background_image field or the default value if not set.
+ * @return {!sketchology.proto.BackgroundImageInfo} The value.
+ */
+sketchology.proto.PageProperties.prototype.getBackgroundImageOrDefault = function() {
+  return /** @type {!sketchology.proto.BackgroundImageInfo} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the background_image field.
+ * @param {!sketchology.proto.BackgroundImageInfo} value The value.
+ */
+sketchology.proto.PageProperties.prototype.setBackgroundImage = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the background_image field has a value.
+ */
+sketchology.proto.PageProperties.prototype.hasBackgroundImage = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the background_image field.
+ */
+sketchology.proto.PageProperties.prototype.backgroundImageCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the background_image field.
+ */
+sketchology.proto.PageProperties.prototype.clearBackgroundImage = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the bounds field.
+ * @return {?sketchology.proto.Rect} The value.
+ */
+sketchology.proto.PageProperties.prototype.getBounds = function() {
+  return /** @type {?sketchology.proto.Rect} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the bounds field or the default value if not set.
+ * @return {!sketchology.proto.Rect} The value.
+ */
+sketchology.proto.PageProperties.prototype.getBoundsOrDefault = function() {
+  return /** @type {!sketchology.proto.Rect} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the bounds field.
+ * @param {!sketchology.proto.Rect} value The value.
+ */
+sketchology.proto.PageProperties.prototype.setBounds = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the bounds field has a value.
+ */
+sketchology.proto.PageProperties.prototype.hasBounds = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the bounds field.
+ */
+sketchology.proto.PageProperties.prototype.boundsCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the bounds field.
+ */
+sketchology.proto.PageProperties.prototype.clearBounds = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the border field.
+ * @return {?sketchology.proto.Border} The value.
+ */
+sketchology.proto.PageProperties.prototype.getBorder = function() {
+  return /** @type {?sketchology.proto.Border} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the border field or the default value if not set.
+ * @return {!sketchology.proto.Border} The value.
+ */
+sketchology.proto.PageProperties.prototype.getBorderOrDefault = function() {
+  return /** @type {!sketchology.proto.Border} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the border field.
+ * @param {!sketchology.proto.Border} value The value.
+ */
+sketchology.proto.PageProperties.prototype.setBorder = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the border field has a value.
+ */
+sketchology.proto.PageProperties.prototype.hasBorder = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the border field.
+ */
+sketchology.proto.PageProperties.prototype.borderCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the border field.
+ */
+sketchology.proto.PageProperties.prototype.clearBorder = function() {
+  this.clear$Field(4);
+};
+
+
+
+/**
+ * Message AddAction.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.AddAction = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.AddAction, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.AddAction.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.AddAction} The cloned message.
+ * @override
+ */
+sketchology.proto.AddAction.prototype.clone;
+
+
+/**
+ * Gets the value of the uuid field.
+ * @return {?string} The value.
+ */
+sketchology.proto.AddAction.prototype.getUuid = function() {
+  return /** @type {?string} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the uuid field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.AddAction.prototype.getUuidOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the uuid field.
+ * @param {string} value The value.
+ */
+sketchology.proto.AddAction.prototype.setUuid = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uuid field has a value.
+ */
+sketchology.proto.AddAction.prototype.hasUuid = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uuid field.
+ */
+sketchology.proto.AddAction.prototype.uuidCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uuid field.
+ */
+sketchology.proto.AddAction.prototype.clearUuid = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the below_element_with_uuid field.
+ * @return {?string} The value.
+ */
+sketchology.proto.AddAction.prototype.getBelowElementWithUuid = function() {
+  return /** @type {?string} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the below_element_with_uuid field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.AddAction.prototype.getBelowElementWithUuidOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the below_element_with_uuid field.
+ * @param {string} value The value.
+ */
+sketchology.proto.AddAction.prototype.setBelowElementWithUuid = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the below_element_with_uuid field has a value.
+ */
+sketchology.proto.AddAction.prototype.hasBelowElementWithUuid = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the below_element_with_uuid field.
+ */
+sketchology.proto.AddAction.prototype.belowElementWithUuidCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the below_element_with_uuid field.
+ */
+sketchology.proto.AddAction.prototype.clearBelowElementWithUuid = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message RemoveAction.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.RemoveAction = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.RemoveAction, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.RemoveAction.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.RemoveAction} The cloned message.
+ * @override
+ */
+sketchology.proto.RemoveAction.prototype.clone;
+
+
+/**
+ * Gets the value of the uuid field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?string} The value.
+ */
+sketchology.proto.RemoveAction.prototype.getUuid = function(index) {
+  return /** @type {?string} */ (this.get$Value(1, index));
+};
+
+
+/**
+ * Gets the value of the uuid field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {string} The value.
+ */
+sketchology.proto.RemoveAction.prototype.getUuidOrDefault = function(index) {
+  return /** @type {string} */ (this.get$ValueOrDefault(1, index));
+};
+
+
+/**
+ * Adds a value to the uuid field.
+ * @param {string} value The value to add.
+ */
+sketchology.proto.RemoveAction.prototype.addUuid = function(value) {
+  this.add$Value(1, value);
+};
+
+
+/**
+ * Returns the array of values in the uuid field.
+ * @return {!Array<string>} The values in the field.
+ */
+sketchology.proto.RemoveAction.prototype.uuidArray = function() {
+  return /** @type {!Array<string>} */ (this.array$Values(1));
+};
+
+
+/**
+ * @return {boolean} Whether the uuid field has a value.
+ */
+sketchology.proto.RemoveAction.prototype.hasUuid = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uuid field.
+ */
+sketchology.proto.RemoveAction.prototype.uuidCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uuid field.
+ */
+sketchology.proto.RemoveAction.prototype.clearUuid = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the was_below_uuid field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?string} The value.
+ */
+sketchology.proto.RemoveAction.prototype.getWasBelowUuid = function(index) {
+  return /** @type {?string} */ (this.get$Value(2, index));
+};
+
+
+/**
+ * Gets the value of the was_below_uuid field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {string} The value.
+ */
+sketchology.proto.RemoveAction.prototype.getWasBelowUuidOrDefault = function(index) {
+  return /** @type {string} */ (this.get$ValueOrDefault(2, index));
+};
+
+
+/**
+ * Adds a value to the was_below_uuid field.
+ * @param {string} value The value to add.
+ */
+sketchology.proto.RemoveAction.prototype.addWasBelowUuid = function(value) {
+  this.add$Value(2, value);
+};
+
+
+/**
+ * Returns the array of values in the was_below_uuid field.
+ * @return {!Array<string>} The values in the field.
+ */
+sketchology.proto.RemoveAction.prototype.wasBelowUuidArray = function() {
+  return /** @type {!Array<string>} */ (this.array$Values(2));
+};
+
+
+/**
+ * @return {boolean} Whether the was_below_uuid field has a value.
+ */
+sketchology.proto.RemoveAction.prototype.hasWasBelowUuid = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the was_below_uuid field.
+ */
+sketchology.proto.RemoveAction.prototype.wasBelowUuidCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the was_below_uuid field.
+ */
+sketchology.proto.RemoveAction.prototype.clearWasBelowUuid = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message ClearAction.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ClearAction = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ClearAction, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ClearAction.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ClearAction} The cloned message.
+ * @override
+ */
+sketchology.proto.ClearAction.prototype.clone;
+
+
+/**
+ * Gets the value of the uuid field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?string} The value.
+ */
+sketchology.proto.ClearAction.prototype.getUuid = function(index) {
+  return /** @type {?string} */ (this.get$Value(1, index));
+};
+
+
+/**
+ * Gets the value of the uuid field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {string} The value.
+ */
+sketchology.proto.ClearAction.prototype.getUuidOrDefault = function(index) {
+  return /** @type {string} */ (this.get$ValueOrDefault(1, index));
+};
+
+
+/**
+ * Adds a value to the uuid field.
+ * @param {string} value The value to add.
+ */
+sketchology.proto.ClearAction.prototype.addUuid = function(value) {
+  this.add$Value(1, value);
+};
+
+
+/**
+ * Returns the array of values in the uuid field.
+ * @return {!Array<string>} The values in the field.
+ */
+sketchology.proto.ClearAction.prototype.uuidArray = function() {
+  return /** @type {!Array<string>} */ (this.array$Values(1));
+};
+
+
+/**
+ * @return {boolean} Whether the uuid field has a value.
+ */
+sketchology.proto.ClearAction.prototype.hasUuid = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uuid field.
+ */
+sketchology.proto.ClearAction.prototype.uuidCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uuid field.
+ */
+sketchology.proto.ClearAction.prototype.clearUuid = function() {
+  this.clear$Field(1);
+};
+
+
+
+/**
+ * Message ReplaceAction.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ReplaceAction = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ReplaceAction, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ReplaceAction.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ReplaceAction} The cloned message.
+ * @override
+ */
+sketchology.proto.ReplaceAction.prototype.clone;
+
+
+/**
+ * Gets the value of the uuid_add field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?string} The value.
+ */
+sketchology.proto.ReplaceAction.prototype.getUuidAdd = function(index) {
+  return /** @type {?string} */ (this.get$Value(1, index));
+};
+
+
+/**
+ * Gets the value of the uuid_add field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {string} The value.
+ */
+sketchology.proto.ReplaceAction.prototype.getUuidAddOrDefault = function(index) {
+  return /** @type {string} */ (this.get$ValueOrDefault(1, index));
+};
+
+
+/**
+ * Adds a value to the uuid_add field.
+ * @param {string} value The value to add.
+ */
+sketchology.proto.ReplaceAction.prototype.addUuidAdd = function(value) {
+  this.add$Value(1, value);
+};
+
+
+/**
+ * Returns the array of values in the uuid_add field.
+ * @return {!Array<string>} The values in the field.
+ */
+sketchology.proto.ReplaceAction.prototype.uuidAddArray = function() {
+  return /** @type {!Array<string>} */ (this.array$Values(1));
+};
+
+
+/**
+ * @return {boolean} Whether the uuid_add field has a value.
+ */
+sketchology.proto.ReplaceAction.prototype.hasUuidAdd = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uuid_add field.
+ */
+sketchology.proto.ReplaceAction.prototype.uuidAddCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uuid_add field.
+ */
+sketchology.proto.ReplaceAction.prototype.clearUuidAdd = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the below_element_with_uuid field.
+ * @return {?string} The value.
+ */
+sketchology.proto.ReplaceAction.prototype.getBelowElementWithUuid = function() {
+  return /** @type {?string} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the below_element_with_uuid field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.ReplaceAction.prototype.getBelowElementWithUuidOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the below_element_with_uuid field.
+ * @param {string} value The value.
+ */
+sketchology.proto.ReplaceAction.prototype.setBelowElementWithUuid = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the below_element_with_uuid field has a value.
+ */
+sketchology.proto.ReplaceAction.prototype.hasBelowElementWithUuid = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the below_element_with_uuid field.
+ */
+sketchology.proto.ReplaceAction.prototype.belowElementWithUuidCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the below_element_with_uuid field.
+ */
+sketchology.proto.ReplaceAction.prototype.clearBelowElementWithUuid = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the uuid_remove field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?string} The value.
+ */
+sketchology.proto.ReplaceAction.prototype.getUuidRemove = function(index) {
+  return /** @type {?string} */ (this.get$Value(3, index));
+};
+
+
+/**
+ * Gets the value of the uuid_remove field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {string} The value.
+ */
+sketchology.proto.ReplaceAction.prototype.getUuidRemoveOrDefault = function(index) {
+  return /** @type {string} */ (this.get$ValueOrDefault(3, index));
+};
+
+
+/**
+ * Adds a value to the uuid_remove field.
+ * @param {string} value The value to add.
+ */
+sketchology.proto.ReplaceAction.prototype.addUuidRemove = function(value) {
+  this.add$Value(3, value);
+};
+
+
+/**
+ * Returns the array of values in the uuid_remove field.
+ * @return {!Array<string>} The values in the field.
+ */
+sketchology.proto.ReplaceAction.prototype.uuidRemoveArray = function() {
+  return /** @type {!Array<string>} */ (this.array$Values(3));
+};
+
+
+/**
+ * @return {boolean} Whether the uuid_remove field has a value.
+ */
+sketchology.proto.ReplaceAction.prototype.hasUuidRemove = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the uuid_remove field.
+ */
+sketchology.proto.ReplaceAction.prototype.uuidRemoveCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the uuid_remove field.
+ */
+sketchology.proto.ReplaceAction.prototype.clearUuidRemove = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the was_below_uuid field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?string} The value.
+ */
+sketchology.proto.ReplaceAction.prototype.getWasBelowUuid = function(index) {
+  return /** @type {?string} */ (this.get$Value(4, index));
+};
+
+
+/**
+ * Gets the value of the was_below_uuid field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {string} The value.
+ */
+sketchology.proto.ReplaceAction.prototype.getWasBelowUuidOrDefault = function(index) {
+  return /** @type {string} */ (this.get$ValueOrDefault(4, index));
+};
+
+
+/**
+ * Adds a value to the was_below_uuid field.
+ * @param {string} value The value to add.
+ */
+sketchology.proto.ReplaceAction.prototype.addWasBelowUuid = function(value) {
+  this.add$Value(4, value);
+};
+
+
+/**
+ * Returns the array of values in the was_below_uuid field.
+ * @return {!Array<string>} The values in the field.
+ */
+sketchology.proto.ReplaceAction.prototype.wasBelowUuidArray = function() {
+  return /** @type {!Array<string>} */ (this.array$Values(4));
+};
+
+
+/**
+ * @return {boolean} Whether the was_below_uuid field has a value.
+ */
+sketchology.proto.ReplaceAction.prototype.hasWasBelowUuid = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the was_below_uuid field.
+ */
+sketchology.proto.ReplaceAction.prototype.wasBelowUuidCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the was_below_uuid field.
+ */
+sketchology.proto.ReplaceAction.prototype.clearWasBelowUuid = function() {
+  this.clear$Field(4);
+};
+
+
+
+/**
+ * Message SetTransformAction.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.SetTransformAction = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.SetTransformAction, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.SetTransformAction.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.SetTransformAction} The cloned message.
+ * @override
+ */
+sketchology.proto.SetTransformAction.prototype.clone;
+
+
+/**
+ * Gets the value of the uuid field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?string} The value.
+ */
+sketchology.proto.SetTransformAction.prototype.getUuid = function(index) {
+  return /** @type {?string} */ (this.get$Value(1, index));
+};
+
+
+/**
+ * Gets the value of the uuid field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {string} The value.
+ */
+sketchology.proto.SetTransformAction.prototype.getUuidOrDefault = function(index) {
+  return /** @type {string} */ (this.get$ValueOrDefault(1, index));
+};
+
+
+/**
+ * Adds a value to the uuid field.
+ * @param {string} value The value to add.
+ */
+sketchology.proto.SetTransformAction.prototype.addUuid = function(value) {
+  this.add$Value(1, value);
+};
+
+
+/**
+ * Returns the array of values in the uuid field.
+ * @return {!Array<string>} The values in the field.
+ */
+sketchology.proto.SetTransformAction.prototype.uuidArray = function() {
+  return /** @type {!Array<string>} */ (this.array$Values(1));
+};
+
+
+/**
+ * @return {boolean} Whether the uuid field has a value.
+ */
+sketchology.proto.SetTransformAction.prototype.hasUuid = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uuid field.
+ */
+sketchology.proto.SetTransformAction.prototype.uuidCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uuid field.
+ */
+sketchology.proto.SetTransformAction.prototype.clearUuid = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the from_transform field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.AffineTransform} The value.
+ */
+sketchology.proto.SetTransformAction.prototype.getFromTransform = function(index) {
+  return /** @type {?sketchology.proto.AffineTransform} */ (this.get$Value(2, index));
+};
+
+
+/**
+ * Gets the value of the from_transform field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.AffineTransform} The value.
+ */
+sketchology.proto.SetTransformAction.prototype.getFromTransformOrDefault = function(index) {
+  return /** @type {!sketchology.proto.AffineTransform} */ (this.get$ValueOrDefault(2, index));
+};
+
+
+/**
+ * Adds a value to the from_transform field.
+ * @param {!sketchology.proto.AffineTransform} value The value to add.
+ */
+sketchology.proto.SetTransformAction.prototype.addFromTransform = function(value) {
+  this.add$Value(2, value);
+};
+
+
+/**
+ * Returns the array of values in the from_transform field.
+ * @return {!Array<!sketchology.proto.AffineTransform>} The values in the field.
+ */
+sketchology.proto.SetTransformAction.prototype.fromTransformArray = function() {
+  return /** @type {!Array<!sketchology.proto.AffineTransform>} */ (this.array$Values(2));
+};
+
+
+/**
+ * @return {boolean} Whether the from_transform field has a value.
+ */
+sketchology.proto.SetTransformAction.prototype.hasFromTransform = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the from_transform field.
+ */
+sketchology.proto.SetTransformAction.prototype.fromTransformCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the from_transform field.
+ */
+sketchology.proto.SetTransformAction.prototype.clearFromTransform = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the to_transform field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.AffineTransform} The value.
+ */
+sketchology.proto.SetTransformAction.prototype.getToTransform = function(index) {
+  return /** @type {?sketchology.proto.AffineTransform} */ (this.get$Value(3, index));
+};
+
+
+/**
+ * Gets the value of the to_transform field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.AffineTransform} The value.
+ */
+sketchology.proto.SetTransformAction.prototype.getToTransformOrDefault = function(index) {
+  return /** @type {!sketchology.proto.AffineTransform} */ (this.get$ValueOrDefault(3, index));
+};
+
+
+/**
+ * Adds a value to the to_transform field.
+ * @param {!sketchology.proto.AffineTransform} value The value to add.
+ */
+sketchology.proto.SetTransformAction.prototype.addToTransform = function(value) {
+  this.add$Value(3, value);
+};
+
+
+/**
+ * Returns the array of values in the to_transform field.
+ * @return {!Array<!sketchology.proto.AffineTransform>} The values in the field.
+ */
+sketchology.proto.SetTransformAction.prototype.toTransformArray = function() {
+  return /** @type {!Array<!sketchology.proto.AffineTransform>} */ (this.array$Values(3));
+};
+
+
+/**
+ * @return {boolean} Whether the to_transform field has a value.
+ */
+sketchology.proto.SetTransformAction.prototype.hasToTransform = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the to_transform field.
+ */
+sketchology.proto.SetTransformAction.prototype.toTransformCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the to_transform field.
+ */
+sketchology.proto.SetTransformAction.prototype.clearToTransform = function() {
+  this.clear$Field(3);
+};
+
+
+
+/**
+ * Message SetPageBoundsAction.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.SetPageBoundsAction = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.SetPageBoundsAction, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.SetPageBoundsAction.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.SetPageBoundsAction} The cloned message.
+ * @override
+ */
+sketchology.proto.SetPageBoundsAction.prototype.clone;
+
+
+/**
+ * Gets the value of the old_bounds field.
+ * @return {?sketchology.proto.Rect} The value.
+ */
+sketchology.proto.SetPageBoundsAction.prototype.getOldBounds = function() {
+  return /** @type {?sketchology.proto.Rect} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the old_bounds field or the default value if not set.
+ * @return {!sketchology.proto.Rect} The value.
+ */
+sketchology.proto.SetPageBoundsAction.prototype.getOldBoundsOrDefault = function() {
+  return /** @type {!sketchology.proto.Rect} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the old_bounds field.
+ * @param {!sketchology.proto.Rect} value The value.
+ */
+sketchology.proto.SetPageBoundsAction.prototype.setOldBounds = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the old_bounds field has a value.
+ */
+sketchology.proto.SetPageBoundsAction.prototype.hasOldBounds = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the old_bounds field.
+ */
+sketchology.proto.SetPageBoundsAction.prototype.oldBoundsCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the old_bounds field.
+ */
+sketchology.proto.SetPageBoundsAction.prototype.clearOldBounds = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the new_bounds field.
+ * @return {?sketchology.proto.Rect} The value.
+ */
+sketchology.proto.SetPageBoundsAction.prototype.getNewBounds = function() {
+  return /** @type {?sketchology.proto.Rect} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the new_bounds field or the default value if not set.
+ * @return {!sketchology.proto.Rect} The value.
+ */
+sketchology.proto.SetPageBoundsAction.prototype.getNewBoundsOrDefault = function() {
+  return /** @type {!sketchology.proto.Rect} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the new_bounds field.
+ * @param {!sketchology.proto.Rect} value The value.
+ */
+sketchology.proto.SetPageBoundsAction.prototype.setNewBounds = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the new_bounds field has a value.
+ */
+sketchology.proto.SetPageBoundsAction.prototype.hasNewBounds = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the new_bounds field.
+ */
+sketchology.proto.SetPageBoundsAction.prototype.newBoundsCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the new_bounds field.
+ */
+sketchology.proto.SetPageBoundsAction.prototype.clearNewBounds = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message StorageAction.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.StorageAction = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.StorageAction, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.StorageAction.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.StorageAction} The cloned message.
+ * @override
+ */
+sketchology.proto.StorageAction.prototype.clone;
+
+
+/**
+ * Gets the value of the add_action field.
+ * @return {?sketchology.proto.AddAction} The value.
+ */
+sketchology.proto.StorageAction.prototype.getAddAction = function() {
+  return /** @type {?sketchology.proto.AddAction} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the add_action field or the default value if not set.
+ * @return {!sketchology.proto.AddAction} The value.
+ */
+sketchology.proto.StorageAction.prototype.getAddActionOrDefault = function() {
+  return /** @type {!sketchology.proto.AddAction} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the add_action field.
+ * @param {!sketchology.proto.AddAction} value The value.
+ */
+sketchology.proto.StorageAction.prototype.setAddAction = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the add_action field has a value.
+ */
+sketchology.proto.StorageAction.prototype.hasAddAction = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the add_action field.
+ */
+sketchology.proto.StorageAction.prototype.addActionCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the add_action field.
+ */
+sketchology.proto.StorageAction.prototype.clearAddAction = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the remove_action field.
+ * @return {?sketchology.proto.RemoveAction} The value.
+ */
+sketchology.proto.StorageAction.prototype.getRemoveAction = function() {
+  return /** @type {?sketchology.proto.RemoveAction} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the remove_action field or the default value if not set.
+ * @return {!sketchology.proto.RemoveAction} The value.
+ */
+sketchology.proto.StorageAction.prototype.getRemoveActionOrDefault = function() {
+  return /** @type {!sketchology.proto.RemoveAction} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the remove_action field.
+ * @param {!sketchology.proto.RemoveAction} value The value.
+ */
+sketchology.proto.StorageAction.prototype.setRemoveAction = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the remove_action field has a value.
+ */
+sketchology.proto.StorageAction.prototype.hasRemoveAction = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the remove_action field.
+ */
+sketchology.proto.StorageAction.prototype.removeActionCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the remove_action field.
+ */
+sketchology.proto.StorageAction.prototype.clearRemoveAction = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the clear_action field.
+ * @return {?sketchology.proto.ClearAction} The value.
+ */
+sketchology.proto.StorageAction.prototype.getClearAction = function() {
+  return /** @type {?sketchology.proto.ClearAction} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the clear_action field or the default value if not set.
+ * @return {!sketchology.proto.ClearAction} The value.
+ */
+sketchology.proto.StorageAction.prototype.getClearActionOrDefault = function() {
+  return /** @type {!sketchology.proto.ClearAction} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the clear_action field.
+ * @param {!sketchology.proto.ClearAction} value The value.
+ */
+sketchology.proto.StorageAction.prototype.setClearAction = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the clear_action field has a value.
+ */
+sketchology.proto.StorageAction.prototype.hasClearAction = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the clear_action field.
+ */
+sketchology.proto.StorageAction.prototype.clearActionCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the clear_action field.
+ */
+sketchology.proto.StorageAction.prototype.clearClearAction = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the replace_action field.
+ * @return {?sketchology.proto.ReplaceAction} The value.
+ */
+sketchology.proto.StorageAction.prototype.getReplaceAction = function() {
+  return /** @type {?sketchology.proto.ReplaceAction} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the replace_action field or the default value if not set.
+ * @return {!sketchology.proto.ReplaceAction} The value.
+ */
+sketchology.proto.StorageAction.prototype.getReplaceActionOrDefault = function() {
+  return /** @type {!sketchology.proto.ReplaceAction} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the replace_action field.
+ * @param {!sketchology.proto.ReplaceAction} value The value.
+ */
+sketchology.proto.StorageAction.prototype.setReplaceAction = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the replace_action field has a value.
+ */
+sketchology.proto.StorageAction.prototype.hasReplaceAction = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the replace_action field.
+ */
+sketchology.proto.StorageAction.prototype.replaceActionCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the replace_action field.
+ */
+sketchology.proto.StorageAction.prototype.clearReplaceAction = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Gets the value of the set_transform_action field.
+ * @return {?sketchology.proto.SetTransformAction} The value.
+ */
+sketchology.proto.StorageAction.prototype.getSetTransformAction = function() {
+  return /** @type {?sketchology.proto.SetTransformAction} */ (this.get$Value(5));
+};
+
+
+/**
+ * Gets the value of the set_transform_action field or the default value if not set.
+ * @return {!sketchology.proto.SetTransformAction} The value.
+ */
+sketchology.proto.StorageAction.prototype.getSetTransformActionOrDefault = function() {
+  return /** @type {!sketchology.proto.SetTransformAction} */ (this.get$ValueOrDefault(5));
+};
+
+
+/**
+ * Sets the value of the set_transform_action field.
+ * @param {!sketchology.proto.SetTransformAction} value The value.
+ */
+sketchology.proto.StorageAction.prototype.setSetTransformAction = function(value) {
+  this.set$Value(5, value);
+};
+
+
+/**
+ * @return {boolean} Whether the set_transform_action field has a value.
+ */
+sketchology.proto.StorageAction.prototype.hasSetTransformAction = function() {
+  return this.has$Value(5);
+};
+
+
+/**
+ * @return {number} The number of values in the set_transform_action field.
+ */
+sketchology.proto.StorageAction.prototype.setTransformActionCount = function() {
+  return this.count$Values(5);
+};
+
+
+/**
+ * Clears the values in the set_transform_action field.
+ */
+sketchology.proto.StorageAction.prototype.clearSetTransformAction = function() {
+  this.clear$Field(5);
+};
+
+
+/**
+ * Gets the value of the set_page_bounds_action field.
+ * @return {?sketchology.proto.SetPageBoundsAction} The value.
+ */
+sketchology.proto.StorageAction.prototype.getSetPageBoundsAction = function() {
+  return /** @type {?sketchology.proto.SetPageBoundsAction} */ (this.get$Value(6));
+};
+
+
+/**
+ * Gets the value of the set_page_bounds_action field or the default value if not set.
+ * @return {!sketchology.proto.SetPageBoundsAction} The value.
+ */
+sketchology.proto.StorageAction.prototype.getSetPageBoundsActionOrDefault = function() {
+  return /** @type {!sketchology.proto.SetPageBoundsAction} */ (this.get$ValueOrDefault(6));
+};
+
+
+/**
+ * Sets the value of the set_page_bounds_action field.
+ * @param {!sketchology.proto.SetPageBoundsAction} value The value.
+ */
+sketchology.proto.StorageAction.prototype.setSetPageBoundsAction = function(value) {
+  this.set$Value(6, value);
+};
+
+
+/**
+ * @return {boolean} Whether the set_page_bounds_action field has a value.
+ */
+sketchology.proto.StorageAction.prototype.hasSetPageBoundsAction = function() {
+  return this.has$Value(6);
+};
+
+
+/**
+ * @return {number} The number of values in the set_page_bounds_action field.
+ */
+sketchology.proto.StorageAction.prototype.setPageBoundsActionCount = function() {
+  return this.count$Values(6);
+};
+
+
+/**
+ * Clears the values in the set_page_bounds_action field.
+ */
+sketchology.proto.StorageAction.prototype.clearSetPageBoundsAction = function() {
+  this.clear$Field(6);
+};
+
+
+
+/**
+ * Message Snapshot.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.Snapshot = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.Snapshot, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.Snapshot.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.Snapshot} The cloned message.
+ * @override
+ */
+sketchology.proto.Snapshot.prototype.clone;
+
+
+/**
+ * Gets the value of the page_properties field.
+ * @return {?sketchology.proto.PageProperties} The value.
+ */
+sketchology.proto.Snapshot.prototype.getPageProperties = function() {
+  return /** @type {?sketchology.proto.PageProperties} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the page_properties field or the default value if not set.
+ * @return {!sketchology.proto.PageProperties} The value.
+ */
+sketchology.proto.Snapshot.prototype.getPagePropertiesOrDefault = function() {
+  return /** @type {!sketchology.proto.PageProperties} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the page_properties field.
+ * @param {!sketchology.proto.PageProperties} value The value.
+ */
+sketchology.proto.Snapshot.prototype.setPageProperties = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the page_properties field has a value.
+ */
+sketchology.proto.Snapshot.prototype.hasPageProperties = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the page_properties field.
+ */
+sketchology.proto.Snapshot.prototype.pagePropertiesCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the page_properties field.
+ */
+sketchology.proto.Snapshot.prototype.clearPageProperties = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the element field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.ElementBundle} The value.
+ */
+sketchology.proto.Snapshot.prototype.getElement = function(index) {
+  return /** @type {?sketchology.proto.ElementBundle} */ (this.get$Value(2, index));
+};
+
+
+/**
+ * Gets the value of the element field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.ElementBundle} The value.
+ */
+sketchology.proto.Snapshot.prototype.getElementOrDefault = function(index) {
+  return /** @type {!sketchology.proto.ElementBundle} */ (this.get$ValueOrDefault(2, index));
+};
+
+
+/**
+ * Adds a value to the element field.
+ * @param {!sketchology.proto.ElementBundle} value The value to add.
+ */
+sketchology.proto.Snapshot.prototype.addElement = function(value) {
+  this.add$Value(2, value);
+};
+
+
+/**
+ * Returns the array of values in the element field.
+ * @return {!Array<!sketchology.proto.ElementBundle>} The values in the field.
+ */
+sketchology.proto.Snapshot.prototype.elementArray = function() {
+  return /** @type {!Array<!sketchology.proto.ElementBundle>} */ (this.array$Values(2));
+};
+
+
+/**
+ * @return {boolean} Whether the element field has a value.
+ */
+sketchology.proto.Snapshot.prototype.hasElement = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the element field.
+ */
+sketchology.proto.Snapshot.prototype.elementCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the element field.
+ */
+sketchology.proto.Snapshot.prototype.clearElement = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the dead_element field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.ElementBundle} The value.
+ */
+sketchology.proto.Snapshot.prototype.getDeadElement = function(index) {
+  return /** @type {?sketchology.proto.ElementBundle} */ (this.get$Value(3, index));
+};
+
+
+/**
+ * Gets the value of the dead_element field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.ElementBundle} The value.
+ */
+sketchology.proto.Snapshot.prototype.getDeadElementOrDefault = function(index) {
+  return /** @type {!sketchology.proto.ElementBundle} */ (this.get$ValueOrDefault(3, index));
+};
+
+
+/**
+ * Adds a value to the dead_element field.
+ * @param {!sketchology.proto.ElementBundle} value The value to add.
+ */
+sketchology.proto.Snapshot.prototype.addDeadElement = function(value) {
+  this.add$Value(3, value);
+};
+
+
+/**
+ * Returns the array of values in the dead_element field.
+ * @return {!Array<!sketchology.proto.ElementBundle>} The values in the field.
+ */
+sketchology.proto.Snapshot.prototype.deadElementArray = function() {
+  return /** @type {!Array<!sketchology.proto.ElementBundle>} */ (this.array$Values(3));
+};
+
+
+/**
+ * @return {boolean} Whether the dead_element field has a value.
+ */
+sketchology.proto.Snapshot.prototype.hasDeadElement = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the dead_element field.
+ */
+sketchology.proto.Snapshot.prototype.deadElementCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the dead_element field.
+ */
+sketchology.proto.Snapshot.prototype.clearDeadElement = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the undo_action field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.StorageAction} The value.
+ */
+sketchology.proto.Snapshot.prototype.getUndoAction = function(index) {
+  return /** @type {?sketchology.proto.StorageAction} */ (this.get$Value(4, index));
+};
+
+
+/**
+ * Gets the value of the undo_action field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.StorageAction} The value.
+ */
+sketchology.proto.Snapshot.prototype.getUndoActionOrDefault = function(index) {
+  return /** @type {!sketchology.proto.StorageAction} */ (this.get$ValueOrDefault(4, index));
+};
+
+
+/**
+ * Adds a value to the undo_action field.
+ * @param {!sketchology.proto.StorageAction} value The value to add.
+ */
+sketchology.proto.Snapshot.prototype.addUndoAction = function(value) {
+  this.add$Value(4, value);
+};
+
+
+/**
+ * Returns the array of values in the undo_action field.
+ * @return {!Array<!sketchology.proto.StorageAction>} The values in the field.
+ */
+sketchology.proto.Snapshot.prototype.undoActionArray = function() {
+  return /** @type {!Array<!sketchology.proto.StorageAction>} */ (this.array$Values(4));
+};
+
+
+/**
+ * @return {boolean} Whether the undo_action field has a value.
+ */
+sketchology.proto.Snapshot.prototype.hasUndoAction = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the undo_action field.
+ */
+sketchology.proto.Snapshot.prototype.undoActionCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the undo_action field.
+ */
+sketchology.proto.Snapshot.prototype.clearUndoAction = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Gets the value of the redo_action field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.StorageAction} The value.
+ */
+sketchology.proto.Snapshot.prototype.getRedoAction = function(index) {
+  return /** @type {?sketchology.proto.StorageAction} */ (this.get$Value(5, index));
+};
+
+
+/**
+ * Gets the value of the redo_action field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.StorageAction} The value.
+ */
+sketchology.proto.Snapshot.prototype.getRedoActionOrDefault = function(index) {
+  return /** @type {!sketchology.proto.StorageAction} */ (this.get$ValueOrDefault(5, index));
+};
+
+
+/**
+ * Adds a value to the redo_action field.
+ * @param {!sketchology.proto.StorageAction} value The value to add.
+ */
+sketchology.proto.Snapshot.prototype.addRedoAction = function(value) {
+  this.add$Value(5, value);
+};
+
+
+/**
+ * Returns the array of values in the redo_action field.
+ * @return {!Array<!sketchology.proto.StorageAction>} The values in the field.
+ */
+sketchology.proto.Snapshot.prototype.redoActionArray = function() {
+  return /** @type {!Array<!sketchology.proto.StorageAction>} */ (this.array$Values(5));
+};
+
+
+/**
+ * @return {boolean} Whether the redo_action field has a value.
+ */
+sketchology.proto.Snapshot.prototype.hasRedoAction = function() {
+  return this.has$Value(5);
+};
+
+
+/**
+ * @return {number} The number of values in the redo_action field.
+ */
+sketchology.proto.Snapshot.prototype.redoActionCount = function() {
+  return this.count$Values(5);
+};
+
+
+/**
+ * Clears the values in the redo_action field.
+ */
+sketchology.proto.Snapshot.prototype.clearRedoAction = function() {
+  this.clear$Field(5);
+};
+
+
+/**
+ * Gets the value of the element_state_index field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.ElementState} The value.
+ */
+sketchology.proto.Snapshot.prototype.getElementStateIndex = function(index) {
+  return /** @type {?sketchology.proto.ElementState} */ (this.get$Value(6, index));
+};
+
+
+/**
+ * Gets the value of the element_state_index field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.ElementState} The value.
+ */
+sketchology.proto.Snapshot.prototype.getElementStateIndexOrDefault = function(index) {
+  return /** @type {!sketchology.proto.ElementState} */ (this.get$ValueOrDefault(6, index));
+};
+
+
+/**
+ * Adds a value to the element_state_index field.
+ * @param {!sketchology.proto.ElementState} value The value to add.
+ */
+sketchology.proto.Snapshot.prototype.addElementStateIndex = function(value) {
+  this.add$Value(6, value);
+};
+
+
+/**
+ * Returns the array of values in the element_state_index field.
+ * @return {!Array<!sketchology.proto.ElementState>} The values in the field.
+ */
+sketchology.proto.Snapshot.prototype.elementStateIndexArray = function() {
+  return /** @type {!Array<!sketchology.proto.ElementState>} */ (this.array$Values(6));
+};
+
+
+/**
+ * @return {boolean} Whether the element_state_index field has a value.
+ */
+sketchology.proto.Snapshot.prototype.hasElementStateIndex = function() {
+  return this.has$Value(6);
+};
+
+
+/**
+ * @return {number} The number of values in the element_state_index field.
+ */
+sketchology.proto.Snapshot.prototype.elementStateIndexCount = function() {
+  return this.count$Values(6);
+};
+
+
+/**
+ * Clears the values in the element_state_index field.
+ */
+sketchology.proto.Snapshot.prototype.clearElementStateIndex = function() {
+  this.clear$Field(6);
+};
+
+
+/**
+ * Gets the value of the fingerprint field.
+ * @return {?string} The value.
+ */
+sketchology.proto.Snapshot.prototype.getFingerprint = function() {
+  return /** @type {?string} */ (this.get$Value(7));
+};
+
+
+/**
+ * Gets the value of the fingerprint field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.Snapshot.prototype.getFingerprintOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(7));
+};
+
+
+/**
+ * Sets the value of the fingerprint field.
+ * @param {string} value The value.
+ */
+sketchology.proto.Snapshot.prototype.setFingerprint = function(value) {
+  this.set$Value(7, value);
+};
+
+
+/**
+ * @return {boolean} Whether the fingerprint field has a value.
+ */
+sketchology.proto.Snapshot.prototype.hasFingerprint = function() {
+  return this.has$Value(7);
+};
+
+
+/**
+ * @return {number} The number of values in the fingerprint field.
+ */
+sketchology.proto.Snapshot.prototype.fingerprintCount = function() {
+  return this.count$Values(7);
+};
+
+
+/**
+ * Clears the values in the fingerprint field.
+ */
+sketchology.proto.Snapshot.prototype.clearFingerprint = function() {
+  this.clear$Field(7);
+};
+
+
+
+/**
+ * Message MutationPacket.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.MutationPacket = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.MutationPacket, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.MutationPacket.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.MutationPacket} The cloned message.
+ * @override
+ */
+sketchology.proto.MutationPacket.prototype.clone;
+
+
+/**
+ * Gets the value of the mutation field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.StorageAction} The value.
+ */
+sketchology.proto.MutationPacket.prototype.getMutation = function(index) {
+  return /** @type {?sketchology.proto.StorageAction} */ (this.get$Value(1, index));
+};
+
+
+/**
+ * Gets the value of the mutation field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.StorageAction} The value.
+ */
+sketchology.proto.MutationPacket.prototype.getMutationOrDefault = function(index) {
+  return /** @type {!sketchology.proto.StorageAction} */ (this.get$ValueOrDefault(1, index));
+};
+
+
+/**
+ * Adds a value to the mutation field.
+ * @param {!sketchology.proto.StorageAction} value The value to add.
+ */
+sketchology.proto.MutationPacket.prototype.addMutation = function(value) {
+  this.add$Value(1, value);
+};
+
+
+/**
+ * Returns the array of values in the mutation field.
+ * @return {!Array<!sketchology.proto.StorageAction>} The values in the field.
+ */
+sketchology.proto.MutationPacket.prototype.mutationArray = function() {
+  return /** @type {!Array<!sketchology.proto.StorageAction>} */ (this.array$Values(1));
+};
+
+
+/**
+ * @return {boolean} Whether the mutation field has a value.
+ */
+sketchology.proto.MutationPacket.prototype.hasMutation = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the mutation field.
+ */
+sketchology.proto.MutationPacket.prototype.mutationCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the mutation field.
+ */
+sketchology.proto.MutationPacket.prototype.clearMutation = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the element field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.ElementBundle} The value.
+ */
+sketchology.proto.MutationPacket.prototype.getElement = function(index) {
+  return /** @type {?sketchology.proto.ElementBundle} */ (this.get$Value(2, index));
+};
+
+
+/**
+ * Gets the value of the element field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.ElementBundle} The value.
+ */
+sketchology.proto.MutationPacket.prototype.getElementOrDefault = function(index) {
+  return /** @type {!sketchology.proto.ElementBundle} */ (this.get$ValueOrDefault(2, index));
+};
+
+
+/**
+ * Adds a value to the element field.
+ * @param {!sketchology.proto.ElementBundle} value The value to add.
+ */
+sketchology.proto.MutationPacket.prototype.addElement = function(value) {
+  this.add$Value(2, value);
+};
+
+
+/**
+ * Returns the array of values in the element field.
+ * @return {!Array<!sketchology.proto.ElementBundle>} The values in the field.
+ */
+sketchology.proto.MutationPacket.prototype.elementArray = function() {
+  return /** @type {!Array<!sketchology.proto.ElementBundle>} */ (this.array$Values(2));
+};
+
+
+/**
+ * @return {boolean} Whether the element field has a value.
+ */
+sketchology.proto.MutationPacket.prototype.hasElement = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the element field.
+ */
+sketchology.proto.MutationPacket.prototype.elementCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the element field.
+ */
+sketchology.proto.MutationPacket.prototype.clearElement = function() {
+  this.clear$Field(2);
+};
+
+
+/** @override */
+sketchology.proto.Color.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.Color.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'Color',
+        fullName: 'sketchology.proto.Color'
+      },
+      1: {
+        name: 'argb',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      }
+    };
+    sketchology.proto.Color.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.Color, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.Color.getDescriptor =
+    sketchology.proto.Color.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.BackgroundColor.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.BackgroundColor.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'BackgroundColor',
+        fullName: 'sketchology.proto.BackgroundColor'
+      },
+      1: {
+        name: 'rgba',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      }
+    };
+    sketchology.proto.BackgroundColor.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.BackgroundColor, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.BackgroundColor.getDescriptor =
+    sketchology.proto.BackgroundColor.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.PageProperties.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.PageProperties.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'PageProperties',
+        fullName: 'sketchology.proto.PageProperties'
+      },
+      1: {
+        name: 'background_color',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Color
+      },
+      2: {
+        name: 'background_image',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.BackgroundImageInfo
+      },
+      3: {
+        name: 'bounds',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Rect
+      },
+      4: {
+        name: 'border',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Border
+      }
+    };
+    sketchology.proto.PageProperties.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.PageProperties, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.PageProperties.getDescriptor =
+    sketchology.proto.PageProperties.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.AddAction.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.AddAction.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'AddAction',
+        fullName: 'sketchology.proto.AddAction'
+      },
+      1: {
+        name: 'uuid',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      2: {
+        name: 'below_element_with_uuid',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      }
+    };
+    sketchology.proto.AddAction.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.AddAction, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.AddAction.getDescriptor =
+    sketchology.proto.AddAction.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.RemoveAction.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.RemoveAction.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'RemoveAction',
+        fullName: 'sketchology.proto.RemoveAction'
+      },
+      1: {
+        name: 'uuid',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      2: {
+        name: 'was_below_uuid',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      }
+    };
+    sketchology.proto.RemoveAction.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.RemoveAction, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.RemoveAction.getDescriptor =
+    sketchology.proto.RemoveAction.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ClearAction.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ClearAction.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ClearAction',
+        fullName: 'sketchology.proto.ClearAction'
+      },
+      1: {
+        name: 'uuid',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      }
+    };
+    sketchology.proto.ClearAction.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ClearAction, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ClearAction.getDescriptor =
+    sketchology.proto.ClearAction.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ReplaceAction.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ReplaceAction.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ReplaceAction',
+        fullName: 'sketchology.proto.ReplaceAction'
+      },
+      1: {
+        name: 'uuid_add',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      2: {
+        name: 'below_element_with_uuid',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      3: {
+        name: 'uuid_remove',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      4: {
+        name: 'was_below_uuid',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      }
+    };
+    sketchology.proto.ReplaceAction.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ReplaceAction, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ReplaceAction.getDescriptor =
+    sketchology.proto.ReplaceAction.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.SetTransformAction.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.SetTransformAction.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'SetTransformAction',
+        fullName: 'sketchology.proto.SetTransformAction'
+      },
+      1: {
+        name: 'uuid',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      2: {
+        name: 'from_transform',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.AffineTransform
+      },
+      3: {
+        name: 'to_transform',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.AffineTransform
+      }
+    };
+    sketchology.proto.SetTransformAction.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.SetTransformAction, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.SetTransformAction.getDescriptor =
+    sketchology.proto.SetTransformAction.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.SetPageBoundsAction.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.SetPageBoundsAction.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'SetPageBoundsAction',
+        fullName: 'sketchology.proto.SetPageBoundsAction'
+      },
+      1: {
+        name: 'old_bounds',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Rect
+      },
+      2: {
+        name: 'new_bounds',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Rect
+      }
+    };
+    sketchology.proto.SetPageBoundsAction.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.SetPageBoundsAction, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.SetPageBoundsAction.getDescriptor =
+    sketchology.proto.SetPageBoundsAction.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.StorageAction.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.StorageAction.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'StorageAction',
+        fullName: 'sketchology.proto.StorageAction'
+      },
+      1: {
+        name: 'add_action',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.AddAction
+      },
+      2: {
+        name: 'remove_action',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.RemoveAction
+      },
+      3: {
+        name: 'clear_action',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ClearAction
+      },
+      4: {
+        name: 'replace_action',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ReplaceAction
+      },
+      5: {
+        name: 'set_transform_action',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.SetTransformAction
+      },
+      6: {
+        name: 'set_page_bounds_action',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.SetPageBoundsAction
+      }
+    };
+    sketchology.proto.StorageAction.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.StorageAction, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.StorageAction.getDescriptor =
+    sketchology.proto.StorageAction.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.Snapshot.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.Snapshot.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'Snapshot',
+        fullName: 'sketchology.proto.Snapshot'
+      },
+      1: {
+        name: 'page_properties',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.PageProperties
+      },
+      2: {
+        name: 'element',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ElementBundle
+      },
+      3: {
+        name: 'dead_element',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ElementBundle
+      },
+      4: {
+        name: 'undo_action',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.StorageAction
+      },
+      5: {
+        name: 'redo_action',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.StorageAction
+      },
+      6: {
+        name: 'element_state_index',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: sketchology.proto.ElementState.ALIVE,
+        type: sketchology.proto.ElementState
+      },
+      7: {
+        name: 'fingerprint',
+        fieldType: goog.proto2.Message.FieldType.UINT64,
+        type: String
+      }
+    };
+    sketchology.proto.Snapshot.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.Snapshot, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.Snapshot.getDescriptor =
+    sketchology.proto.Snapshot.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.MutationPacket.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.MutationPacket.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'MutationPacket',
+        fullName: 'sketchology.proto.MutationPacket'
+      },
+      1: {
+        name: 'mutation',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.StorageAction
+      },
+      2: {
+        name: 'element',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ElementBundle
+      }
+    };
+    sketchology.proto.MutationPacket.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.MutationPacket, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.MutationPacket.getDescriptor =
+    sketchology.proto.MutationPacket.prototype.getDescriptor;
diff --git a/third_party/ink/sketchology/proto/elements.pb.js b/third_party/ink/sketchology/proto/elements.pb.js
new file mode 100644
index 0000000..4c0de6a6
--- /dev/null
+++ b/third_party/ink/sketchology/proto/elements.pb.js
@@ -0,0 +1,3976 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// Protocol Buffer 2 Copyright 2008 Google Inc.
+// All other code copyright its respective owners.
+
+/**
+ * @fileoverview Generated Protocol Buffer code for file
+ * third_party/sketchology/proto/elements.proto.
+ * Generated by //net/proto2/compiler/public:protocol_compiler.
+ * @suppress {messageConventions} 
+ */
+
+goog.provide('sketchology.proto.CallbackFlags');
+goog.provide('sketchology.proto.SourceDetails');
+goog.provide('sketchology.proto.SourceDetails.Origin');
+goog.provide('sketchology.proto.BackgroundImageInfo');
+goog.provide('sketchology.proto.Border');
+goog.provide('sketchology.proto.LOD');
+goog.provide('sketchology.proto.Stroke');
+goog.provide('sketchology.proto.UncompressedStroke');
+goog.provide('sketchology.proto.AffineTransform');
+goog.provide('sketchology.proto.Element');
+goog.provide('sketchology.proto.ElementAttributes');
+goog.provide('sketchology.proto.UncompressedElement');
+goog.provide('sketchology.proto.ElementMutation');
+goog.provide('sketchology.proto.ElementIdList');
+goog.provide('sketchology.proto.Point');
+goog.provide('sketchology.proto.ElementBundle');
+goog.provide('sketchology.proto.Path');
+goog.provide('sketchology.proto.Path.SegmentType');
+goog.provide('sketchology.proto.Path.EndCapType');
+goog.provide('sketchology.proto.ShaderType');
+
+goog.require('goog.proto2.Message');
+goog.require('sketchology.proto.Rect');
+
+
+/**
+ * Enumeration ShaderType.
+ * @enum {number}
+ */
+sketchology.proto.ShaderType = {
+  NONE: 0,
+  VERTEX_COLORED: 1,
+  SOLID_COLORED: 2,
+  ERASE: 3,
+  VERTEX_TEXTURED: 4
+};
+
+
+
+/**
+ * Message CallbackFlags.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.CallbackFlags = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.CallbackFlags, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.CallbackFlags.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.CallbackFlags} The cloned message.
+ * @override
+ */
+sketchology.proto.CallbackFlags.prototype.clone;
+
+
+/**
+ * Gets the value of the mesh_data_ctm field.
+ * @return {?boolean} The value.
+ */
+sketchology.proto.CallbackFlags.prototype.getMeshDataCtm = function() {
+  return /** @type {?boolean} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the mesh_data_ctm field or the default value if not set.
+ * @return {boolean} The value.
+ */
+sketchology.proto.CallbackFlags.prototype.getMeshDataCtmOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the mesh_data_ctm field.
+ * @param {boolean} value The value.
+ */
+sketchology.proto.CallbackFlags.prototype.setMeshDataCtm = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the mesh_data_ctm field has a value.
+ */
+sketchology.proto.CallbackFlags.prototype.hasMeshDataCtm = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the mesh_data_ctm field.
+ */
+sketchology.proto.CallbackFlags.prototype.meshDataCtmCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the mesh_data_ctm field.
+ */
+sketchology.proto.CallbackFlags.prototype.clearMeshDataCtm = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the uncompressed_outline field.
+ * @return {?boolean} The value.
+ */
+sketchology.proto.CallbackFlags.prototype.getUncompressedOutline = function() {
+  return /** @type {?boolean} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the uncompressed_outline field or the default value if not set.
+ * @return {boolean} The value.
+ */
+sketchology.proto.CallbackFlags.prototype.getUncompressedOutlineOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the uncompressed_outline field.
+ * @param {boolean} value The value.
+ */
+sketchology.proto.CallbackFlags.prototype.setUncompressedOutline = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uncompressed_outline field has a value.
+ */
+sketchology.proto.CallbackFlags.prototype.hasUncompressedOutline = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the uncompressed_outline field.
+ */
+sketchology.proto.CallbackFlags.prototype.uncompressedOutlineCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the uncompressed_outline field.
+ */
+sketchology.proto.CallbackFlags.prototype.clearUncompressedOutline = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the compressed_input_points field.
+ * @return {?boolean} The value.
+ */
+sketchology.proto.CallbackFlags.prototype.getCompressedInputPoints = function() {
+  return /** @type {?boolean} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the compressed_input_points field or the default value if not set.
+ * @return {boolean} The value.
+ */
+sketchology.proto.CallbackFlags.prototype.getCompressedInputPointsOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the compressed_input_points field.
+ * @param {boolean} value The value.
+ */
+sketchology.proto.CallbackFlags.prototype.setCompressedInputPoints = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the compressed_input_points field has a value.
+ */
+sketchology.proto.CallbackFlags.prototype.hasCompressedInputPoints = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the compressed_input_points field.
+ */
+sketchology.proto.CallbackFlags.prototype.compressedInputPointsCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the compressed_input_points field.
+ */
+sketchology.proto.CallbackFlags.prototype.clearCompressedInputPoints = function() {
+  this.clear$Field(3);
+};
+
+
+
+/**
+ * Message SourceDetails.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.SourceDetails = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.SourceDetails, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.SourceDetails.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.SourceDetails} The cloned message.
+ * @override
+ */
+sketchology.proto.SourceDetails.prototype.clone;
+
+
+/**
+ * Gets the value of the origin field.
+ * @return {?sketchology.proto.SourceDetails.Origin} The value.
+ */
+sketchology.proto.SourceDetails.prototype.getOrigin = function() {
+  return /** @type {?sketchology.proto.SourceDetails.Origin} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the origin field or the default value if not set.
+ * @return {!sketchology.proto.SourceDetails.Origin} The value.
+ */
+sketchology.proto.SourceDetails.prototype.getOriginOrDefault = function() {
+  return /** @type {!sketchology.proto.SourceDetails.Origin} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the origin field.
+ * @param {!sketchology.proto.SourceDetails.Origin} value The value.
+ */
+sketchology.proto.SourceDetails.prototype.setOrigin = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the origin field has a value.
+ */
+sketchology.proto.SourceDetails.prototype.hasOrigin = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the origin field.
+ */
+sketchology.proto.SourceDetails.prototype.originCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the origin field.
+ */
+sketchology.proto.SourceDetails.prototype.clearOrigin = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the host_source_details field.
+ * @return {?number} The value.
+ */
+sketchology.proto.SourceDetails.prototype.getHostSourceDetails = function() {
+  return /** @type {?number} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the host_source_details field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.SourceDetails.prototype.getHostSourceDetailsOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the host_source_details field.
+ * @param {number} value The value.
+ */
+sketchology.proto.SourceDetails.prototype.setHostSourceDetails = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the host_source_details field has a value.
+ */
+sketchology.proto.SourceDetails.prototype.hasHostSourceDetails = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the host_source_details field.
+ */
+sketchology.proto.SourceDetails.prototype.hostSourceDetailsCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the host_source_details field.
+ */
+sketchology.proto.SourceDetails.prototype.clearHostSourceDetails = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Enumeration Origin.
+ * @enum {number}
+ */
+sketchology.proto.SourceDetails.Origin = {
+  UNKNOWN: 0,
+  ENGINE: 1,
+  HOST: 2
+};
+
+
+
+/**
+ * Message BackgroundImageInfo.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.BackgroundImageInfo = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.BackgroundImageInfo, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.BackgroundImageInfo.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.BackgroundImageInfo} The cloned message.
+ * @override
+ */
+sketchology.proto.BackgroundImageInfo.prototype.clone;
+
+
+/**
+ * Gets the value of the uri field.
+ * @return {?string} The value.
+ */
+sketchology.proto.BackgroundImageInfo.prototype.getUri = function() {
+  return /** @type {?string} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the uri field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.BackgroundImageInfo.prototype.getUriOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the uri field.
+ * @param {string} value The value.
+ */
+sketchology.proto.BackgroundImageInfo.prototype.setUri = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uri field has a value.
+ */
+sketchology.proto.BackgroundImageInfo.prototype.hasUri = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uri field.
+ */
+sketchology.proto.BackgroundImageInfo.prototype.uriCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uri field.
+ */
+sketchology.proto.BackgroundImageInfo.prototype.clearUri = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the bounds field.
+ * @return {?sketchology.proto.Rect} The value.
+ */
+sketchology.proto.BackgroundImageInfo.prototype.getBounds = function() {
+  return /** @type {?sketchology.proto.Rect} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the bounds field or the default value if not set.
+ * @return {!sketchology.proto.Rect} The value.
+ */
+sketchology.proto.BackgroundImageInfo.prototype.getBoundsOrDefault = function() {
+  return /** @type {!sketchology.proto.Rect} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the bounds field.
+ * @param {!sketchology.proto.Rect} value The value.
+ */
+sketchology.proto.BackgroundImageInfo.prototype.setBounds = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the bounds field has a value.
+ */
+sketchology.proto.BackgroundImageInfo.prototype.hasBounds = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the bounds field.
+ */
+sketchology.proto.BackgroundImageInfo.prototype.boundsCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the bounds field.
+ */
+sketchology.proto.BackgroundImageInfo.prototype.clearBounds = function() {
+  this.clear$Field(3);
+};
+
+
+
+/**
+ * Message Border.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.Border = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.Border, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.Border.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.Border} The cloned message.
+ * @override
+ */
+sketchology.proto.Border.prototype.clone;
+
+
+/**
+ * Gets the value of the uri field.
+ * @return {?string} The value.
+ */
+sketchology.proto.Border.prototype.getUri = function() {
+  return /** @type {?string} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the uri field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.Border.prototype.getUriOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the uri field.
+ * @param {string} value The value.
+ */
+sketchology.proto.Border.prototype.setUri = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uri field has a value.
+ */
+sketchology.proto.Border.prototype.hasUri = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uri field.
+ */
+sketchology.proto.Border.prototype.uriCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uri field.
+ */
+sketchology.proto.Border.prototype.clearUri = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the scale field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Border.prototype.getScale = function() {
+  return /** @type {?number} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the scale field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Border.prototype.getScaleOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the scale field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Border.prototype.setScale = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the scale field has a value.
+ */
+sketchology.proto.Border.prototype.hasScale = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the scale field.
+ */
+sketchology.proto.Border.prototype.scaleCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the scale field.
+ */
+sketchology.proto.Border.prototype.clearScale = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message LOD.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.LOD = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.LOD, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.LOD.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.LOD} The cloned message.
+ * @override
+ */
+sketchology.proto.LOD.prototype.clone;
+
+
+/**
+ * Gets the value of the max_coverage field.
+ * @return {?number} The value.
+ */
+sketchology.proto.LOD.prototype.getMaxCoverage = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the max_coverage field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.LOD.prototype.getMaxCoverageOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the max_coverage field.
+ * @param {number} value The value.
+ */
+sketchology.proto.LOD.prototype.setMaxCoverage = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the max_coverage field has a value.
+ */
+sketchology.proto.LOD.prototype.hasMaxCoverage = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the max_coverage field.
+ */
+sketchology.proto.LOD.prototype.maxCoverageCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the max_coverage field.
+ */
+sketchology.proto.LOD.prototype.clearMaxCoverage = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the ctm_blob field.
+ * @return {?string} The value.
+ */
+sketchology.proto.LOD.prototype.getCtmBlob = function() {
+  return /** @type {?string} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the ctm_blob field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.LOD.prototype.getCtmBlobOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the ctm_blob field.
+ * @param {string} value The value.
+ */
+sketchology.proto.LOD.prototype.setCtmBlob = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the ctm_blob field has a value.
+ */
+sketchology.proto.LOD.prototype.hasCtmBlob = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the ctm_blob field.
+ */
+sketchology.proto.LOD.prototype.ctmBlobCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the ctm_blob field.
+ */
+sketchology.proto.LOD.prototype.clearCtmBlob = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message Stroke.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.Stroke = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.Stroke, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.Stroke.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.Stroke} The cloned message.
+ * @override
+ */
+sketchology.proto.Stroke.prototype.clone;
+
+
+/**
+ * Gets the value of the shader_type field.
+ * @return {?sketchology.proto.ShaderType} The value.
+ */
+sketchology.proto.Stroke.prototype.getShaderType = function() {
+  return /** @type {?sketchology.proto.ShaderType} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the shader_type field or the default value if not set.
+ * @return {!sketchology.proto.ShaderType} The value.
+ */
+sketchology.proto.Stroke.prototype.getShaderTypeOrDefault = function() {
+  return /** @type {!sketchology.proto.ShaderType} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the shader_type field.
+ * @param {!sketchology.proto.ShaderType} value The value.
+ */
+sketchology.proto.Stroke.prototype.setShaderType = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the shader_type field has a value.
+ */
+sketchology.proto.Stroke.prototype.hasShaderType = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the shader_type field.
+ */
+sketchology.proto.Stroke.prototype.shaderTypeCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the shader_type field.
+ */
+sketchology.proto.Stroke.prototype.clearShaderType = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the lod field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.LOD} The value.
+ */
+sketchology.proto.Stroke.prototype.getLod = function(index) {
+  return /** @type {?sketchology.proto.LOD} */ (this.get$Value(3, index));
+};
+
+
+/**
+ * Gets the value of the lod field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.LOD} The value.
+ */
+sketchology.proto.Stroke.prototype.getLodOrDefault = function(index) {
+  return /** @type {!sketchology.proto.LOD} */ (this.get$ValueOrDefault(3, index));
+};
+
+
+/**
+ * Adds a value to the lod field.
+ * @param {!sketchology.proto.LOD} value The value to add.
+ */
+sketchology.proto.Stroke.prototype.addLod = function(value) {
+  this.add$Value(3, value);
+};
+
+
+/**
+ * Returns the array of values in the lod field.
+ * @return {!Array<!sketchology.proto.LOD>} The values in the field.
+ */
+sketchology.proto.Stroke.prototype.lodArray = function() {
+  return /** @type {!Array<!sketchology.proto.LOD>} */ (this.array$Values(3));
+};
+
+
+/**
+ * @return {boolean} Whether the lod field has a value.
+ */
+sketchology.proto.Stroke.prototype.hasLod = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the lod field.
+ */
+sketchology.proto.Stroke.prototype.lodCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the lod field.
+ */
+sketchology.proto.Stroke.prototype.clearLod = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the abgr field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Stroke.prototype.getAbgr = function() {
+  return /** @type {?number} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the abgr field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Stroke.prototype.getAbgrOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the abgr field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Stroke.prototype.setAbgr = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the abgr field has a value.
+ */
+sketchology.proto.Stroke.prototype.hasAbgr = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the abgr field.
+ */
+sketchology.proto.Stroke.prototype.abgrCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the abgr field.
+ */
+sketchology.proto.Stroke.prototype.clearAbgr = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Gets the value of the point_x field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?number} The value.
+ */
+sketchology.proto.Stroke.prototype.getPointX = function(index) {
+  return /** @type {?number} */ (this.get$Value(5, index));
+};
+
+
+/**
+ * Gets the value of the point_x field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {number} The value.
+ */
+sketchology.proto.Stroke.prototype.getPointXOrDefault = function(index) {
+  return /** @type {number} */ (this.get$ValueOrDefault(5, index));
+};
+
+
+/**
+ * Adds a value to the point_x field.
+ * @param {number} value The value to add.
+ */
+sketchology.proto.Stroke.prototype.addPointX = function(value) {
+  this.add$Value(5, value);
+};
+
+
+/**
+ * Returns the array of values in the point_x field.
+ * @return {!Array<number>} The values in the field.
+ */
+sketchology.proto.Stroke.prototype.pointXArray = function() {
+  return /** @type {!Array<number>} */ (this.array$Values(5));
+};
+
+
+/**
+ * @return {boolean} Whether the point_x field has a value.
+ */
+sketchology.proto.Stroke.prototype.hasPointX = function() {
+  return this.has$Value(5);
+};
+
+
+/**
+ * @return {number} The number of values in the point_x field.
+ */
+sketchology.proto.Stroke.prototype.pointXCount = function() {
+  return this.count$Values(5);
+};
+
+
+/**
+ * Clears the values in the point_x field.
+ */
+sketchology.proto.Stroke.prototype.clearPointX = function() {
+  this.clear$Field(5);
+};
+
+
+/**
+ * Gets the value of the point_y field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?number} The value.
+ */
+sketchology.proto.Stroke.prototype.getPointY = function(index) {
+  return /** @type {?number} */ (this.get$Value(6, index));
+};
+
+
+/**
+ * Gets the value of the point_y field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {number} The value.
+ */
+sketchology.proto.Stroke.prototype.getPointYOrDefault = function(index) {
+  return /** @type {number} */ (this.get$ValueOrDefault(6, index));
+};
+
+
+/**
+ * Adds a value to the point_y field.
+ * @param {number} value The value to add.
+ */
+sketchology.proto.Stroke.prototype.addPointY = function(value) {
+  this.add$Value(6, value);
+};
+
+
+/**
+ * Returns the array of values in the point_y field.
+ * @return {!Array<number>} The values in the field.
+ */
+sketchology.proto.Stroke.prototype.pointYArray = function() {
+  return /** @type {!Array<number>} */ (this.array$Values(6));
+};
+
+
+/**
+ * @return {boolean} Whether the point_y field has a value.
+ */
+sketchology.proto.Stroke.prototype.hasPointY = function() {
+  return this.has$Value(6);
+};
+
+
+/**
+ * @return {number} The number of values in the point_y field.
+ */
+sketchology.proto.Stroke.prototype.pointYCount = function() {
+  return this.count$Values(6);
+};
+
+
+/**
+ * Clears the values in the point_y field.
+ */
+sketchology.proto.Stroke.prototype.clearPointY = function() {
+  this.clear$Field(6);
+};
+
+
+/**
+ * Gets the value of the point_t_ms field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?number} The value.
+ */
+sketchology.proto.Stroke.prototype.getPointTMs = function(index) {
+  return /** @type {?number} */ (this.get$Value(7, index));
+};
+
+
+/**
+ * Gets the value of the point_t_ms field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {number} The value.
+ */
+sketchology.proto.Stroke.prototype.getPointTMsOrDefault = function(index) {
+  return /** @type {number} */ (this.get$ValueOrDefault(7, index));
+};
+
+
+/**
+ * Adds a value to the point_t_ms field.
+ * @param {number} value The value to add.
+ */
+sketchology.proto.Stroke.prototype.addPointTMs = function(value) {
+  this.add$Value(7, value);
+};
+
+
+/**
+ * Returns the array of values in the point_t_ms field.
+ * @return {!Array<number>} The values in the field.
+ */
+sketchology.proto.Stroke.prototype.pointTMsArray = function() {
+  return /** @type {!Array<number>} */ (this.array$Values(7));
+};
+
+
+/**
+ * @return {boolean} Whether the point_t_ms field has a value.
+ */
+sketchology.proto.Stroke.prototype.hasPointTMs = function() {
+  return this.has$Value(7);
+};
+
+
+/**
+ * @return {number} The number of values in the point_t_ms field.
+ */
+sketchology.proto.Stroke.prototype.pointTMsCount = function() {
+  return this.count$Values(7);
+};
+
+
+/**
+ * Clears the values in the point_t_ms field.
+ */
+sketchology.proto.Stroke.prototype.clearPointTMs = function() {
+  this.clear$Field(7);
+};
+
+
+/**
+ * Gets the value of the deprecated_transform field.
+ * @return {?sketchology.proto.AffineTransform} The value.
+ */
+sketchology.proto.Stroke.prototype.getDeprecatedTransform = function() {
+  return /** @type {?sketchology.proto.AffineTransform} */ (this.get$Value(8));
+};
+
+
+/**
+ * Gets the value of the deprecated_transform field or the default value if not set.
+ * @return {!sketchology.proto.AffineTransform} The value.
+ */
+sketchology.proto.Stroke.prototype.getDeprecatedTransformOrDefault = function() {
+  return /** @type {!sketchology.proto.AffineTransform} */ (this.get$ValueOrDefault(8));
+};
+
+
+/**
+ * Sets the value of the deprecated_transform field.
+ * @param {!sketchology.proto.AffineTransform} value The value.
+ */
+sketchology.proto.Stroke.prototype.setDeprecatedTransform = function(value) {
+  this.set$Value(8, value);
+};
+
+
+/**
+ * @return {boolean} Whether the deprecated_transform field has a value.
+ */
+sketchology.proto.Stroke.prototype.hasDeprecatedTransform = function() {
+  return this.has$Value(8);
+};
+
+
+/**
+ * @return {number} The number of values in the deprecated_transform field.
+ */
+sketchology.proto.Stroke.prototype.deprecatedTransformCount = function() {
+  return this.count$Values(8);
+};
+
+
+/**
+ * Clears the values in the deprecated_transform field.
+ */
+sketchology.proto.Stroke.prototype.clearDeprecatedTransform = function() {
+  this.clear$Field(8);
+};
+
+
+/**
+ * Gets the value of the start_time_ms field.
+ * @return {?string} The value.
+ */
+sketchology.proto.Stroke.prototype.getStartTimeMs = function() {
+  return /** @type {?string} */ (this.get$Value(9));
+};
+
+
+/**
+ * Gets the value of the start_time_ms field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.Stroke.prototype.getStartTimeMsOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(9));
+};
+
+
+/**
+ * Sets the value of the start_time_ms field.
+ * @param {string} value The value.
+ */
+sketchology.proto.Stroke.prototype.setStartTimeMs = function(value) {
+  this.set$Value(9, value);
+};
+
+
+/**
+ * @return {boolean} Whether the start_time_ms field has a value.
+ */
+sketchology.proto.Stroke.prototype.hasStartTimeMs = function() {
+  return this.has$Value(9);
+};
+
+
+/**
+ * @return {number} The number of values in the start_time_ms field.
+ */
+sketchology.proto.Stroke.prototype.startTimeMsCount = function() {
+  return this.count$Values(9);
+};
+
+
+/**
+ * Clears the values in the start_time_ms field.
+ */
+sketchology.proto.Stroke.prototype.clearStartTimeMs = function() {
+  this.clear$Field(9);
+};
+
+
+
+/**
+ * Message UncompressedStroke.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.UncompressedStroke = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.UncompressedStroke, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.UncompressedStroke.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.UncompressedStroke} The cloned message.
+ * @override
+ */
+sketchology.proto.UncompressedStroke.prototype.clone;
+
+
+/**
+ * Gets the value of the outline field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.Point} The value.
+ */
+sketchology.proto.UncompressedStroke.prototype.getOutline = function(index) {
+  return /** @type {?sketchology.proto.Point} */ (this.get$Value(1, index));
+};
+
+
+/**
+ * Gets the value of the outline field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.Point} The value.
+ */
+sketchology.proto.UncompressedStroke.prototype.getOutlineOrDefault = function(index) {
+  return /** @type {!sketchology.proto.Point} */ (this.get$ValueOrDefault(1, index));
+};
+
+
+/**
+ * Adds a value to the outline field.
+ * @param {!sketchology.proto.Point} value The value to add.
+ */
+sketchology.proto.UncompressedStroke.prototype.addOutline = function(value) {
+  this.add$Value(1, value);
+};
+
+
+/**
+ * Returns the array of values in the outline field.
+ * @return {!Array<!sketchology.proto.Point>} The values in the field.
+ */
+sketchology.proto.UncompressedStroke.prototype.outlineArray = function() {
+  return /** @type {!Array<!sketchology.proto.Point>} */ (this.array$Values(1));
+};
+
+
+/**
+ * @return {boolean} Whether the outline field has a value.
+ */
+sketchology.proto.UncompressedStroke.prototype.hasOutline = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the outline field.
+ */
+sketchology.proto.UncompressedStroke.prototype.outlineCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the outline field.
+ */
+sketchology.proto.UncompressedStroke.prototype.clearOutline = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the rgba field.
+ * @return {?number} The value.
+ */
+sketchology.proto.UncompressedStroke.prototype.getRgba = function() {
+  return /** @type {?number} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the rgba field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.UncompressedStroke.prototype.getRgbaOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the rgba field.
+ * @param {number} value The value.
+ */
+sketchology.proto.UncompressedStroke.prototype.setRgba = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the rgba field has a value.
+ */
+sketchology.proto.UncompressedStroke.prototype.hasRgba = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the rgba field.
+ */
+sketchology.proto.UncompressedStroke.prototype.rgbaCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the rgba field.
+ */
+sketchology.proto.UncompressedStroke.prototype.clearRgba = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message AffineTransform.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.AffineTransform = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.AffineTransform, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.AffineTransform.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.AffineTransform} The cloned message.
+ * @override
+ */
+sketchology.proto.AffineTransform.prototype.clone;
+
+
+/**
+ * Gets the value of the tx field.
+ * @return {?number} The value.
+ */
+sketchology.proto.AffineTransform.prototype.getTx = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the tx field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.AffineTransform.prototype.getTxOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the tx field.
+ * @param {number} value The value.
+ */
+sketchology.proto.AffineTransform.prototype.setTx = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the tx field has a value.
+ */
+sketchology.proto.AffineTransform.prototype.hasTx = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the tx field.
+ */
+sketchology.proto.AffineTransform.prototype.txCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the tx field.
+ */
+sketchology.proto.AffineTransform.prototype.clearTx = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the ty field.
+ * @return {?number} The value.
+ */
+sketchology.proto.AffineTransform.prototype.getTy = function() {
+  return /** @type {?number} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the ty field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.AffineTransform.prototype.getTyOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the ty field.
+ * @param {number} value The value.
+ */
+sketchology.proto.AffineTransform.prototype.setTy = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the ty field has a value.
+ */
+sketchology.proto.AffineTransform.prototype.hasTy = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the ty field.
+ */
+sketchology.proto.AffineTransform.prototype.tyCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the ty field.
+ */
+sketchology.proto.AffineTransform.prototype.clearTy = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the scale_x field.
+ * @return {?number} The value.
+ */
+sketchology.proto.AffineTransform.prototype.getScaleX = function() {
+  return /** @type {?number} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the scale_x field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.AffineTransform.prototype.getScaleXOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the scale_x field.
+ * @param {number} value The value.
+ */
+sketchology.proto.AffineTransform.prototype.setScaleX = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the scale_x field has a value.
+ */
+sketchology.proto.AffineTransform.prototype.hasScaleX = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the scale_x field.
+ */
+sketchology.proto.AffineTransform.prototype.scaleXCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the scale_x field.
+ */
+sketchology.proto.AffineTransform.prototype.clearScaleX = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the scale_y field.
+ * @return {?number} The value.
+ */
+sketchology.proto.AffineTransform.prototype.getScaleY = function() {
+  return /** @type {?number} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the scale_y field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.AffineTransform.prototype.getScaleYOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the scale_y field.
+ * @param {number} value The value.
+ */
+sketchology.proto.AffineTransform.prototype.setScaleY = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the scale_y field has a value.
+ */
+sketchology.proto.AffineTransform.prototype.hasScaleY = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the scale_y field.
+ */
+sketchology.proto.AffineTransform.prototype.scaleYCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the scale_y field.
+ */
+sketchology.proto.AffineTransform.prototype.clearScaleY = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Gets the value of the rotation_radians field.
+ * @return {?number} The value.
+ */
+sketchology.proto.AffineTransform.prototype.getRotationRadians = function() {
+  return /** @type {?number} */ (this.get$Value(5));
+};
+
+
+/**
+ * Gets the value of the rotation_radians field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.AffineTransform.prototype.getRotationRadiansOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(5));
+};
+
+
+/**
+ * Sets the value of the rotation_radians field.
+ * @param {number} value The value.
+ */
+sketchology.proto.AffineTransform.prototype.setRotationRadians = function(value) {
+  this.set$Value(5, value);
+};
+
+
+/**
+ * @return {boolean} Whether the rotation_radians field has a value.
+ */
+sketchology.proto.AffineTransform.prototype.hasRotationRadians = function() {
+  return this.has$Value(5);
+};
+
+
+/**
+ * @return {number} The number of values in the rotation_radians field.
+ */
+sketchology.proto.AffineTransform.prototype.rotationRadiansCount = function() {
+  return this.count$Values(5);
+};
+
+
+/**
+ * Clears the values in the rotation_radians field.
+ */
+sketchology.proto.AffineTransform.prototype.clearRotationRadians = function() {
+  this.clear$Field(5);
+};
+
+
+
+/**
+ * Message Element.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.Element = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.Element, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.Element.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.Element} The cloned message.
+ * @override
+ */
+sketchology.proto.Element.prototype.clone;
+
+
+/**
+ * Gets the value of the deprecated_uuid field.
+ * @return {?string} The value.
+ */
+sketchology.proto.Element.prototype.getDeprecatedUuid = function() {
+  return /** @type {?string} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the deprecated_uuid field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.Element.prototype.getDeprecatedUuidOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the deprecated_uuid field.
+ * @param {string} value The value.
+ */
+sketchology.proto.Element.prototype.setDeprecatedUuid = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the deprecated_uuid field has a value.
+ */
+sketchology.proto.Element.prototype.hasDeprecatedUuid = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the deprecated_uuid field.
+ */
+sketchology.proto.Element.prototype.deprecatedUuidCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the deprecated_uuid field.
+ */
+sketchology.proto.Element.prototype.clearDeprecatedUuid = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Gets the value of the minimum_serializer_version field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Element.prototype.getMinimumSerializerVersion = function() {
+  return /** @type {?number} */ (this.get$Value(5));
+};
+
+
+/**
+ * Gets the value of the minimum_serializer_version field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Element.prototype.getMinimumSerializerVersionOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(5));
+};
+
+
+/**
+ * Sets the value of the minimum_serializer_version field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Element.prototype.setMinimumSerializerVersion = function(value) {
+  this.set$Value(5, value);
+};
+
+
+/**
+ * @return {boolean} Whether the minimum_serializer_version field has a value.
+ */
+sketchology.proto.Element.prototype.hasMinimumSerializerVersion = function() {
+  return this.has$Value(5);
+};
+
+
+/**
+ * @return {number} The number of values in the minimum_serializer_version field.
+ */
+sketchology.proto.Element.prototype.minimumSerializerVersionCount = function() {
+  return this.count$Values(5);
+};
+
+
+/**
+ * Clears the values in the minimum_serializer_version field.
+ */
+sketchology.proto.Element.prototype.clearMinimumSerializerVersion = function() {
+  this.clear$Field(5);
+};
+
+
+/**
+ * Gets the value of the stroke field.
+ * @return {?sketchology.proto.Stroke} The value.
+ */
+sketchology.proto.Element.prototype.getStroke = function() {
+  return /** @type {?sketchology.proto.Stroke} */ (this.get$Value(6));
+};
+
+
+/**
+ * Gets the value of the stroke field or the default value if not set.
+ * @return {!sketchology.proto.Stroke} The value.
+ */
+sketchology.proto.Element.prototype.getStrokeOrDefault = function() {
+  return /** @type {!sketchology.proto.Stroke} */ (this.get$ValueOrDefault(6));
+};
+
+
+/**
+ * Sets the value of the stroke field.
+ * @param {!sketchology.proto.Stroke} value The value.
+ */
+sketchology.proto.Element.prototype.setStroke = function(value) {
+  this.set$Value(6, value);
+};
+
+
+/**
+ * @return {boolean} Whether the stroke field has a value.
+ */
+sketchology.proto.Element.prototype.hasStroke = function() {
+  return this.has$Value(6);
+};
+
+
+/**
+ * @return {number} The number of values in the stroke field.
+ */
+sketchology.proto.Element.prototype.strokeCount = function() {
+  return this.count$Values(6);
+};
+
+
+/**
+ * Clears the values in the stroke field.
+ */
+sketchology.proto.Element.prototype.clearStroke = function() {
+  this.clear$Field(6);
+};
+
+
+/**
+ * Gets the value of the path field.
+ * @return {?sketchology.proto.Path} The value.
+ */
+sketchology.proto.Element.prototype.getPath = function() {
+  return /** @type {?sketchology.proto.Path} */ (this.get$Value(9));
+};
+
+
+/**
+ * Gets the value of the path field or the default value if not set.
+ * @return {!sketchology.proto.Path} The value.
+ */
+sketchology.proto.Element.prototype.getPathOrDefault = function() {
+  return /** @type {!sketchology.proto.Path} */ (this.get$ValueOrDefault(9));
+};
+
+
+/**
+ * Sets the value of the path field.
+ * @param {!sketchology.proto.Path} value The value.
+ */
+sketchology.proto.Element.prototype.setPath = function(value) {
+  this.set$Value(9, value);
+};
+
+
+/**
+ * @return {boolean} Whether the path field has a value.
+ */
+sketchology.proto.Element.prototype.hasPath = function() {
+  return this.has$Value(9);
+};
+
+
+/**
+ * @return {number} The number of values in the path field.
+ */
+sketchology.proto.Element.prototype.pathCount = function() {
+  return this.count$Values(9);
+};
+
+
+/**
+ * Clears the values in the path field.
+ */
+sketchology.proto.Element.prototype.clearPath = function() {
+  this.clear$Field(9);
+};
+
+
+/**
+ * Gets the value of the attributes field.
+ * @return {?sketchology.proto.ElementAttributes} The value.
+ */
+sketchology.proto.Element.prototype.getAttributes = function() {
+  return /** @type {?sketchology.proto.ElementAttributes} */ (this.get$Value(10));
+};
+
+
+/**
+ * Gets the value of the attributes field or the default value if not set.
+ * @return {!sketchology.proto.ElementAttributes} The value.
+ */
+sketchology.proto.Element.prototype.getAttributesOrDefault = function() {
+  return /** @type {!sketchology.proto.ElementAttributes} */ (this.get$ValueOrDefault(10));
+};
+
+
+/**
+ * Sets the value of the attributes field.
+ * @param {!sketchology.proto.ElementAttributes} value The value.
+ */
+sketchology.proto.Element.prototype.setAttributes = function(value) {
+  this.set$Value(10, value);
+};
+
+
+/**
+ * @return {boolean} Whether the attributes field has a value.
+ */
+sketchology.proto.Element.prototype.hasAttributes = function() {
+  return this.has$Value(10);
+};
+
+
+/**
+ * @return {number} The number of values in the attributes field.
+ */
+sketchology.proto.Element.prototype.attributesCount = function() {
+  return this.count$Values(10);
+};
+
+
+/**
+ * Clears the values in the attributes field.
+ */
+sketchology.proto.Element.prototype.clearAttributes = function() {
+  this.clear$Field(10);
+};
+
+
+
+/**
+ * Message ElementAttributes.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ElementAttributes = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ElementAttributes, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ElementAttributes.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ElementAttributes} The cloned message.
+ * @override
+ */
+sketchology.proto.ElementAttributes.prototype.clone;
+
+
+/**
+ * Gets the value of the selectable field.
+ * @return {?boolean} The value.
+ */
+sketchology.proto.ElementAttributes.prototype.getSelectable = function() {
+  return /** @type {?boolean} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the selectable field or the default value if not set.
+ * @return {boolean} The value.
+ */
+sketchology.proto.ElementAttributes.prototype.getSelectableOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the selectable field.
+ * @param {boolean} value The value.
+ */
+sketchology.proto.ElementAttributes.prototype.setSelectable = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the selectable field has a value.
+ */
+sketchology.proto.ElementAttributes.prototype.hasSelectable = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the selectable field.
+ */
+sketchology.proto.ElementAttributes.prototype.selectableCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the selectable field.
+ */
+sketchology.proto.ElementAttributes.prototype.clearSelectable = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the magic_erasable field.
+ * @return {?boolean} The value.
+ */
+sketchology.proto.ElementAttributes.prototype.getMagicErasable = function() {
+  return /** @type {?boolean} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the magic_erasable field or the default value if not set.
+ * @return {boolean} The value.
+ */
+sketchology.proto.ElementAttributes.prototype.getMagicErasableOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the magic_erasable field.
+ * @param {boolean} value The value.
+ */
+sketchology.proto.ElementAttributes.prototype.setMagicErasable = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the magic_erasable field has a value.
+ */
+sketchology.proto.ElementAttributes.prototype.hasMagicErasable = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the magic_erasable field.
+ */
+sketchology.proto.ElementAttributes.prototype.magicErasableCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the magic_erasable field.
+ */
+sketchology.proto.ElementAttributes.prototype.clearMagicErasable = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the is_sticker field.
+ * @return {?boolean} The value.
+ */
+sketchology.proto.ElementAttributes.prototype.getIsSticker = function() {
+  return /** @type {?boolean} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the is_sticker field or the default value if not set.
+ * @return {boolean} The value.
+ */
+sketchology.proto.ElementAttributes.prototype.getIsStickerOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the is_sticker field.
+ * @param {boolean} value The value.
+ */
+sketchology.proto.ElementAttributes.prototype.setIsSticker = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the is_sticker field has a value.
+ */
+sketchology.proto.ElementAttributes.prototype.hasIsSticker = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the is_sticker field.
+ */
+sketchology.proto.ElementAttributes.prototype.isStickerCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the is_sticker field.
+ */
+sketchology.proto.ElementAttributes.prototype.clearIsSticker = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the is_text field.
+ * @return {?boolean} The value.
+ */
+sketchology.proto.ElementAttributes.prototype.getIsText = function() {
+  return /** @type {?boolean} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the is_text field or the default value if not set.
+ * @return {boolean} The value.
+ */
+sketchology.proto.ElementAttributes.prototype.getIsTextOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the is_text field.
+ * @param {boolean} value The value.
+ */
+sketchology.proto.ElementAttributes.prototype.setIsText = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the is_text field has a value.
+ */
+sketchology.proto.ElementAttributes.prototype.hasIsText = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the is_text field.
+ */
+sketchology.proto.ElementAttributes.prototype.isTextCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the is_text field.
+ */
+sketchology.proto.ElementAttributes.prototype.clearIsText = function() {
+  this.clear$Field(4);
+};
+
+
+
+/**
+ * Message UncompressedElement.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.UncompressedElement = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.UncompressedElement, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.UncompressedElement.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.UncompressedElement} The cloned message.
+ * @override
+ */
+sketchology.proto.UncompressedElement.prototype.clone;
+
+
+/**
+ * Gets the value of the uncompressed_stroke field.
+ * @return {?sketchology.proto.UncompressedStroke} The value.
+ */
+sketchology.proto.UncompressedElement.prototype.getUncompressedStroke = function() {
+  return /** @type {?sketchology.proto.UncompressedStroke} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the uncompressed_stroke field or the default value if not set.
+ * @return {!sketchology.proto.UncompressedStroke} The value.
+ */
+sketchology.proto.UncompressedElement.prototype.getUncompressedStrokeOrDefault = function() {
+  return /** @type {!sketchology.proto.UncompressedStroke} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the uncompressed_stroke field.
+ * @param {!sketchology.proto.UncompressedStroke} value The value.
+ */
+sketchology.proto.UncompressedElement.prototype.setUncompressedStroke = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uncompressed_stroke field has a value.
+ */
+sketchology.proto.UncompressedElement.prototype.hasUncompressedStroke = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uncompressed_stroke field.
+ */
+sketchology.proto.UncompressedElement.prototype.uncompressedStrokeCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uncompressed_stroke field.
+ */
+sketchology.proto.UncompressedElement.prototype.clearUncompressedStroke = function() {
+  this.clear$Field(1);
+};
+
+
+
+/**
+ * Message ElementMutation.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ElementMutation = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ElementMutation, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ElementMutation.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ElementMutation} The cloned message.
+ * @override
+ */
+sketchology.proto.ElementMutation.prototype.clone;
+
+
+/**
+ * Gets the value of the uuid field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?string} The value.
+ */
+sketchology.proto.ElementMutation.prototype.getUuid = function(index) {
+  return /** @type {?string} */ (this.get$Value(1, index));
+};
+
+
+/**
+ * Gets the value of the uuid field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {string} The value.
+ */
+sketchology.proto.ElementMutation.prototype.getUuidOrDefault = function(index) {
+  return /** @type {string} */ (this.get$ValueOrDefault(1, index));
+};
+
+
+/**
+ * Adds a value to the uuid field.
+ * @param {string} value The value to add.
+ */
+sketchology.proto.ElementMutation.prototype.addUuid = function(value) {
+  this.add$Value(1, value);
+};
+
+
+/**
+ * Returns the array of values in the uuid field.
+ * @return {!Array<string>} The values in the field.
+ */
+sketchology.proto.ElementMutation.prototype.uuidArray = function() {
+  return /** @type {!Array<string>} */ (this.array$Values(1));
+};
+
+
+/**
+ * @return {boolean} Whether the uuid field has a value.
+ */
+sketchology.proto.ElementMutation.prototype.hasUuid = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uuid field.
+ */
+sketchology.proto.ElementMutation.prototype.uuidCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uuid field.
+ */
+sketchology.proto.ElementMutation.prototype.clearUuid = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the transform field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.AffineTransform} The value.
+ */
+sketchology.proto.ElementMutation.prototype.getTransform = function(index) {
+  return /** @type {?sketchology.proto.AffineTransform} */ (this.get$Value(2, index));
+};
+
+
+/**
+ * Gets the value of the transform field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.AffineTransform} The value.
+ */
+sketchology.proto.ElementMutation.prototype.getTransformOrDefault = function(index) {
+  return /** @type {!sketchology.proto.AffineTransform} */ (this.get$ValueOrDefault(2, index));
+};
+
+
+/**
+ * Adds a value to the transform field.
+ * @param {!sketchology.proto.AffineTransform} value The value to add.
+ */
+sketchology.proto.ElementMutation.prototype.addTransform = function(value) {
+  this.add$Value(2, value);
+};
+
+
+/**
+ * Returns the array of values in the transform field.
+ * @return {!Array<!sketchology.proto.AffineTransform>} The values in the field.
+ */
+sketchology.proto.ElementMutation.prototype.transformArray = function() {
+  return /** @type {!Array<!sketchology.proto.AffineTransform>} */ (this.array$Values(2));
+};
+
+
+/**
+ * @return {boolean} Whether the transform field has a value.
+ */
+sketchology.proto.ElementMutation.prototype.hasTransform = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the transform field.
+ */
+sketchology.proto.ElementMutation.prototype.transformCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the transform field.
+ */
+sketchology.proto.ElementMutation.prototype.clearTransform = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message ElementIdList.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ElementIdList = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ElementIdList, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ElementIdList.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ElementIdList} The cloned message.
+ * @override
+ */
+sketchology.proto.ElementIdList.prototype.clone;
+
+
+/**
+ * Gets the value of the uuid field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?string} The value.
+ */
+sketchology.proto.ElementIdList.prototype.getUuid = function(index) {
+  return /** @type {?string} */ (this.get$Value(1, index));
+};
+
+
+/**
+ * Gets the value of the uuid field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {string} The value.
+ */
+sketchology.proto.ElementIdList.prototype.getUuidOrDefault = function(index) {
+  return /** @type {string} */ (this.get$ValueOrDefault(1, index));
+};
+
+
+/**
+ * Adds a value to the uuid field.
+ * @param {string} value The value to add.
+ */
+sketchology.proto.ElementIdList.prototype.addUuid = function(value) {
+  this.add$Value(1, value);
+};
+
+
+/**
+ * Returns the array of values in the uuid field.
+ * @return {!Array<string>} The values in the field.
+ */
+sketchology.proto.ElementIdList.prototype.uuidArray = function() {
+  return /** @type {!Array<string>} */ (this.array$Values(1));
+};
+
+
+/**
+ * @return {boolean} Whether the uuid field has a value.
+ */
+sketchology.proto.ElementIdList.prototype.hasUuid = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uuid field.
+ */
+sketchology.proto.ElementIdList.prototype.uuidCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uuid field.
+ */
+sketchology.proto.ElementIdList.prototype.clearUuid = function() {
+  this.clear$Field(1);
+};
+
+
+
+/**
+ * Message Point.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.Point = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.Point, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.Point.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.Point} The cloned message.
+ * @override
+ */
+sketchology.proto.Point.prototype.clone;
+
+
+/**
+ * Gets the value of the x field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Point.prototype.getX = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the x field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Point.prototype.getXOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the x field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Point.prototype.setX = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the x field has a value.
+ */
+sketchology.proto.Point.prototype.hasX = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the x field.
+ */
+sketchology.proto.Point.prototype.xCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the x field.
+ */
+sketchology.proto.Point.prototype.clearX = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the y field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Point.prototype.getY = function() {
+  return /** @type {?number} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the y field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Point.prototype.getYOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the y field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Point.prototype.setY = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the y field has a value.
+ */
+sketchology.proto.Point.prototype.hasY = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the y field.
+ */
+sketchology.proto.Point.prototype.yCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the y field.
+ */
+sketchology.proto.Point.prototype.clearY = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message ElementBundle.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ElementBundle = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ElementBundle, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ElementBundle.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ElementBundle} The cloned message.
+ * @override
+ */
+sketchology.proto.ElementBundle.prototype.clone;
+
+
+/**
+ * Gets the value of the uuid field.
+ * @return {?string} The value.
+ */
+sketchology.proto.ElementBundle.prototype.getUuid = function() {
+  return /** @type {?string} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the uuid field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.ElementBundle.prototype.getUuidOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the uuid field.
+ * @param {string} value The value.
+ */
+sketchology.proto.ElementBundle.prototype.setUuid = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uuid field has a value.
+ */
+sketchology.proto.ElementBundle.prototype.hasUuid = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uuid field.
+ */
+sketchology.proto.ElementBundle.prototype.uuidCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uuid field.
+ */
+sketchology.proto.ElementBundle.prototype.clearUuid = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the element field.
+ * @return {?sketchology.proto.Element} The value.
+ */
+sketchology.proto.ElementBundle.prototype.getElement = function() {
+  return /** @type {?sketchology.proto.Element} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the element field or the default value if not set.
+ * @return {!sketchology.proto.Element} The value.
+ */
+sketchology.proto.ElementBundle.prototype.getElementOrDefault = function() {
+  return /** @type {!sketchology.proto.Element} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the element field.
+ * @param {!sketchology.proto.Element} value The value.
+ */
+sketchology.proto.ElementBundle.prototype.setElement = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the element field has a value.
+ */
+sketchology.proto.ElementBundle.prototype.hasElement = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the element field.
+ */
+sketchology.proto.ElementBundle.prototype.elementCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the element field.
+ */
+sketchology.proto.ElementBundle.prototype.clearElement = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the transform field.
+ * @return {?sketchology.proto.AffineTransform} The value.
+ */
+sketchology.proto.ElementBundle.prototype.getTransform = function() {
+  return /** @type {?sketchology.proto.AffineTransform} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the transform field or the default value if not set.
+ * @return {!sketchology.proto.AffineTransform} The value.
+ */
+sketchology.proto.ElementBundle.prototype.getTransformOrDefault = function() {
+  return /** @type {!sketchology.proto.AffineTransform} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the transform field.
+ * @param {!sketchology.proto.AffineTransform} value The value.
+ */
+sketchology.proto.ElementBundle.prototype.setTransform = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the transform field has a value.
+ */
+sketchology.proto.ElementBundle.prototype.hasTransform = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the transform field.
+ */
+sketchology.proto.ElementBundle.prototype.transformCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the transform field.
+ */
+sketchology.proto.ElementBundle.prototype.clearTransform = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the uncompressed_element field.
+ * @return {?sketchology.proto.UncompressedElement} The value.
+ */
+sketchology.proto.ElementBundle.prototype.getUncompressedElement = function() {
+  return /** @type {?sketchology.proto.UncompressedElement} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the uncompressed_element field or the default value if not set.
+ * @return {!sketchology.proto.UncompressedElement} The value.
+ */
+sketchology.proto.ElementBundle.prototype.getUncompressedElementOrDefault = function() {
+  return /** @type {!sketchology.proto.UncompressedElement} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the uncompressed_element field.
+ * @param {!sketchology.proto.UncompressedElement} value The value.
+ */
+sketchology.proto.ElementBundle.prototype.setUncompressedElement = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uncompressed_element field has a value.
+ */
+sketchology.proto.ElementBundle.prototype.hasUncompressedElement = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the uncompressed_element field.
+ */
+sketchology.proto.ElementBundle.prototype.uncompressedElementCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the uncompressed_element field.
+ */
+sketchology.proto.ElementBundle.prototype.clearUncompressedElement = function() {
+  this.clear$Field(4);
+};
+
+
+
+/**
+ * Message Path.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.Path = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.Path, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.Path.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.Path} The cloned message.
+ * @override
+ */
+sketchology.proto.Path.prototype.clone;
+
+
+/**
+ * Gets the value of the segment_types field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.Path.SegmentType} The value.
+ */
+sketchology.proto.Path.prototype.getSegmentTypes = function(index) {
+  return /** @type {?sketchology.proto.Path.SegmentType} */ (this.get$Value(1, index));
+};
+
+
+/**
+ * Gets the value of the segment_types field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.Path.SegmentType} The value.
+ */
+sketchology.proto.Path.prototype.getSegmentTypesOrDefault = function(index) {
+  return /** @type {!sketchology.proto.Path.SegmentType} */ (this.get$ValueOrDefault(1, index));
+};
+
+
+/**
+ * Adds a value to the segment_types field.
+ * @param {!sketchology.proto.Path.SegmentType} value The value to add.
+ */
+sketchology.proto.Path.prototype.addSegmentTypes = function(value) {
+  this.add$Value(1, value);
+};
+
+
+/**
+ * Returns the array of values in the segment_types field.
+ * @return {!Array<!sketchology.proto.Path.SegmentType>} The values in the field.
+ */
+sketchology.proto.Path.prototype.segmentTypesArray = function() {
+  return /** @type {!Array<!sketchology.proto.Path.SegmentType>} */ (this.array$Values(1));
+};
+
+
+/**
+ * @return {boolean} Whether the segment_types field has a value.
+ */
+sketchology.proto.Path.prototype.hasSegmentTypes = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the segment_types field.
+ */
+sketchology.proto.Path.prototype.segmentTypesCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the segment_types field.
+ */
+sketchology.proto.Path.prototype.clearSegmentTypes = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the segment_counts field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?number} The value.
+ */
+sketchology.proto.Path.prototype.getSegmentCounts = function(index) {
+  return /** @type {?number} */ (this.get$Value(2, index));
+};
+
+
+/**
+ * Gets the value of the segment_counts field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {number} The value.
+ */
+sketchology.proto.Path.prototype.getSegmentCountsOrDefault = function(index) {
+  return /** @type {number} */ (this.get$ValueOrDefault(2, index));
+};
+
+
+/**
+ * Adds a value to the segment_counts field.
+ * @param {number} value The value to add.
+ */
+sketchology.proto.Path.prototype.addSegmentCounts = function(value) {
+  this.add$Value(2, value);
+};
+
+
+/**
+ * Returns the array of values in the segment_counts field.
+ * @return {!Array<number>} The values in the field.
+ */
+sketchology.proto.Path.prototype.segmentCountsArray = function() {
+  return /** @type {!Array<number>} */ (this.array$Values(2));
+};
+
+
+/**
+ * @return {boolean} Whether the segment_counts field has a value.
+ */
+sketchology.proto.Path.prototype.hasSegmentCounts = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the segment_counts field.
+ */
+sketchology.proto.Path.prototype.segmentCountsCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the segment_counts field.
+ */
+sketchology.proto.Path.prototype.clearSegmentCounts = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the segment_args field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?number} The value.
+ */
+sketchology.proto.Path.prototype.getSegmentArgs = function(index) {
+  return /** @type {?number} */ (this.get$Value(3, index));
+};
+
+
+/**
+ * Gets the value of the segment_args field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {number} The value.
+ */
+sketchology.proto.Path.prototype.getSegmentArgsOrDefault = function(index) {
+  return /** @type {number} */ (this.get$ValueOrDefault(3, index));
+};
+
+
+/**
+ * Adds a value to the segment_args field.
+ * @param {number} value The value to add.
+ */
+sketchology.proto.Path.prototype.addSegmentArgs = function(value) {
+  this.add$Value(3, value);
+};
+
+
+/**
+ * Returns the array of values in the segment_args field.
+ * @return {!Array<number>} The values in the field.
+ */
+sketchology.proto.Path.prototype.segmentArgsArray = function() {
+  return /** @type {!Array<number>} */ (this.array$Values(3));
+};
+
+
+/**
+ * @return {boolean} Whether the segment_args field has a value.
+ */
+sketchology.proto.Path.prototype.hasSegmentArgs = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the segment_args field.
+ */
+sketchology.proto.Path.prototype.segmentArgsCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the segment_args field.
+ */
+sketchology.proto.Path.prototype.clearSegmentArgs = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the radius field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Path.prototype.getRadius = function() {
+  return /** @type {?number} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the radius field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Path.prototype.getRadiusOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the radius field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Path.prototype.setRadius = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the radius field has a value.
+ */
+sketchology.proto.Path.prototype.hasRadius = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the radius field.
+ */
+sketchology.proto.Path.prototype.radiusCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the radius field.
+ */
+sketchology.proto.Path.prototype.clearRadius = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Gets the value of the rgba field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Path.prototype.getRgba = function() {
+  return /** @type {?number} */ (this.get$Value(5));
+};
+
+
+/**
+ * Gets the value of the rgba field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Path.prototype.getRgbaOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(5));
+};
+
+
+/**
+ * Sets the value of the rgba field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Path.prototype.setRgba = function(value) {
+  this.set$Value(5, value);
+};
+
+
+/**
+ * @return {boolean} Whether the rgba field has a value.
+ */
+sketchology.proto.Path.prototype.hasRgba = function() {
+  return this.has$Value(5);
+};
+
+
+/**
+ * @return {number} The number of values in the rgba field.
+ */
+sketchology.proto.Path.prototype.rgbaCount = function() {
+  return this.count$Values(5);
+};
+
+
+/**
+ * Clears the values in the rgba field.
+ */
+sketchology.proto.Path.prototype.clearRgba = function() {
+  this.clear$Field(5);
+};
+
+
+/**
+ * Gets the value of the end_cap field.
+ * @return {?sketchology.proto.Path.EndCapType} The value.
+ */
+sketchology.proto.Path.prototype.getEndCap = function() {
+  return /** @type {?sketchology.proto.Path.EndCapType} */ (this.get$Value(6));
+};
+
+
+/**
+ * Gets the value of the end_cap field or the default value if not set.
+ * @return {!sketchology.proto.Path.EndCapType} The value.
+ */
+sketchology.proto.Path.prototype.getEndCapOrDefault = function() {
+  return /** @type {!sketchology.proto.Path.EndCapType} */ (this.get$ValueOrDefault(6));
+};
+
+
+/**
+ * Sets the value of the end_cap field.
+ * @param {!sketchology.proto.Path.EndCapType} value The value.
+ */
+sketchology.proto.Path.prototype.setEndCap = function(value) {
+  this.set$Value(6, value);
+};
+
+
+/**
+ * @return {boolean} Whether the end_cap field has a value.
+ */
+sketchology.proto.Path.prototype.hasEndCap = function() {
+  return this.has$Value(6);
+};
+
+
+/**
+ * @return {number} The number of values in the end_cap field.
+ */
+sketchology.proto.Path.prototype.endCapCount = function() {
+  return this.count$Values(6);
+};
+
+
+/**
+ * Clears the values in the end_cap field.
+ */
+sketchology.proto.Path.prototype.clearEndCap = function() {
+  this.clear$Field(6);
+};
+
+
+/**
+ * Gets the value of the fill_rgba field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Path.prototype.getFillRgba = function() {
+  return /** @type {?number} */ (this.get$Value(7));
+};
+
+
+/**
+ * Gets the value of the fill_rgba field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Path.prototype.getFillRgbaOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(7));
+};
+
+
+/**
+ * Sets the value of the fill_rgba field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Path.prototype.setFillRgba = function(value) {
+  this.set$Value(7, value);
+};
+
+
+/**
+ * @return {boolean} Whether the fill_rgba field has a value.
+ */
+sketchology.proto.Path.prototype.hasFillRgba = function() {
+  return this.has$Value(7);
+};
+
+
+/**
+ * @return {number} The number of values in the fill_rgba field.
+ */
+sketchology.proto.Path.prototype.fillRgbaCount = function() {
+  return this.count$Values(7);
+};
+
+
+/**
+ * Clears the values in the fill_rgba field.
+ */
+sketchology.proto.Path.prototype.clearFillRgba = function() {
+  this.clear$Field(7);
+};
+
+
+/**
+ * Enumeration SegmentType.
+ * @enum {number}
+ */
+sketchology.proto.Path.SegmentType = {
+  UNKNOWN: 0,
+  MOVE_TO: 1,
+  LINE_TO: 2,
+  CURVE_TO: 3,
+  QUAD_TO: 4,
+  CLOSE: 5
+};
+
+
+/**
+ * Enumeration EndCapType.
+ * @enum {number}
+ */
+sketchology.proto.Path.EndCapType = {
+  BUTT: 1,
+  ROUND: 2,
+  SQUARE: 3
+};
+
+
+/** @override */
+sketchology.proto.CallbackFlags.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.CallbackFlags.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'CallbackFlags',
+        fullName: 'sketchology.proto.CallbackFlags'
+      },
+      1: {
+        name: 'mesh_data_ctm',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        type: Boolean
+      },
+      2: {
+        name: 'uncompressed_outline',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        type: Boolean
+      },
+      3: {
+        name: 'compressed_input_points',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        type: Boolean
+      }
+    };
+    sketchology.proto.CallbackFlags.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.CallbackFlags, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.CallbackFlags.getDescriptor =
+    sketchology.proto.CallbackFlags.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.SourceDetails.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.SourceDetails.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'SourceDetails',
+        fullName: 'sketchology.proto.SourceDetails'
+      },
+      1: {
+        name: 'origin',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: sketchology.proto.SourceDetails.Origin.UNKNOWN,
+        type: sketchology.proto.SourceDetails.Origin
+      },
+      2: {
+        name: 'host_source_details',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      }
+    };
+    sketchology.proto.SourceDetails.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.SourceDetails, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.SourceDetails.getDescriptor =
+    sketchology.proto.SourceDetails.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.BackgroundImageInfo.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.BackgroundImageInfo.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'BackgroundImageInfo',
+        fullName: 'sketchology.proto.BackgroundImageInfo'
+      },
+      1: {
+        name: 'uri',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      3: {
+        name: 'bounds',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Rect
+      }
+    };
+    sketchology.proto.BackgroundImageInfo.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.BackgroundImageInfo, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.BackgroundImageInfo.getDescriptor =
+    sketchology.proto.BackgroundImageInfo.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.Border.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.Border.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'Border',
+        fullName: 'sketchology.proto.Border'
+      },
+      1: {
+        name: 'uri',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      2: {
+        name: 'scale',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        defaultValue: 1,
+        type: Number
+      }
+    };
+    sketchology.proto.Border.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.Border, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.Border.getDescriptor =
+    sketchology.proto.Border.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.LOD.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.LOD.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'LOD',
+        fullName: 'sketchology.proto.LOD'
+      },
+      1: {
+        name: 'max_coverage',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      2: {
+        name: 'ctm_blob',
+        fieldType: goog.proto2.Message.FieldType.BYTES,
+        type: String
+      }
+    };
+    sketchology.proto.LOD.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.LOD, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.LOD.getDescriptor =
+    sketchology.proto.LOD.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.Stroke.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.Stroke.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'Stroke',
+        fullName: 'sketchology.proto.Stroke'
+      },
+      1: {
+        name: 'shader_type',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: sketchology.proto.ShaderType.NONE,
+        type: sketchology.proto.ShaderType
+      },
+      3: {
+        name: 'lod',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.LOD
+      },
+      4: {
+        name: 'abgr',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      },
+      5: {
+        name: 'point_x',
+        repeated: true,
+        packed: true,
+        fieldType: goog.proto2.Message.FieldType.SINT32,
+        type: Number
+      },
+      6: {
+        name: 'point_y',
+        repeated: true,
+        packed: true,
+        fieldType: goog.proto2.Message.FieldType.SINT32,
+        type: Number
+      },
+      7: {
+        name: 'point_t_ms',
+        repeated: true,
+        packed: true,
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      },
+      8: {
+        name: 'deprecated_transform',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.AffineTransform
+      },
+      9: {
+        name: 'start_time_ms',
+        fieldType: goog.proto2.Message.FieldType.UINT64,
+        type: String
+      }
+    };
+    sketchology.proto.Stroke.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.Stroke, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.Stroke.getDescriptor =
+    sketchology.proto.Stroke.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.UncompressedStroke.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.UncompressedStroke.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'UncompressedStroke',
+        fullName: 'sketchology.proto.UncompressedStroke'
+      },
+      1: {
+        name: 'outline',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Point
+      },
+      2: {
+        name: 'rgba',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      }
+    };
+    sketchology.proto.UncompressedStroke.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.UncompressedStroke, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.UncompressedStroke.getDescriptor =
+    sketchology.proto.UncompressedStroke.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.AffineTransform.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.AffineTransform.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'AffineTransform',
+        fullName: 'sketchology.proto.AffineTransform'
+      },
+      1: {
+        name: 'tx',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      2: {
+        name: 'ty',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      3: {
+        name: 'scale_x',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        defaultValue: 1,
+        type: Number
+      },
+      4: {
+        name: 'scale_y',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        defaultValue: 1,
+        type: Number
+      },
+      5: {
+        name: 'rotation_radians',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      }
+    };
+    sketchology.proto.AffineTransform.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.AffineTransform, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.AffineTransform.getDescriptor =
+    sketchology.proto.AffineTransform.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.Element.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.Element.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'Element',
+        fullName: 'sketchology.proto.Element'
+      },
+      4: {
+        name: 'deprecated_uuid',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      5: {
+        name: 'minimum_serializer_version',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      },
+      6: {
+        name: 'stroke',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Stroke
+      },
+      9: {
+        name: 'path',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Path
+      },
+      10: {
+        name: 'attributes',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ElementAttributes
+      }
+    };
+    sketchology.proto.Element.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.Element, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.Element.getDescriptor =
+    sketchology.proto.Element.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ElementAttributes.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ElementAttributes.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ElementAttributes',
+        fullName: 'sketchology.proto.ElementAttributes'
+      },
+      1: {
+        name: 'selectable',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        defaultValue: true,
+        type: Boolean
+      },
+      2: {
+        name: 'magic_erasable',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        defaultValue: true,
+        type: Boolean
+      },
+      3: {
+        name: 'is_sticker',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        defaultValue: false,
+        type: Boolean
+      },
+      4: {
+        name: 'is_text',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        defaultValue: false,
+        type: Boolean
+      }
+    };
+    sketchology.proto.ElementAttributes.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ElementAttributes, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ElementAttributes.getDescriptor =
+    sketchology.proto.ElementAttributes.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.UncompressedElement.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.UncompressedElement.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'UncompressedElement',
+        fullName: 'sketchology.proto.UncompressedElement'
+      },
+      1: {
+        name: 'uncompressed_stroke',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.UncompressedStroke
+      }
+    };
+    sketchology.proto.UncompressedElement.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.UncompressedElement, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.UncompressedElement.getDescriptor =
+    sketchology.proto.UncompressedElement.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ElementMutation.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ElementMutation.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ElementMutation',
+        fullName: 'sketchology.proto.ElementMutation'
+      },
+      1: {
+        name: 'uuid',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      2: {
+        name: 'transform',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.AffineTransform
+      }
+    };
+    sketchology.proto.ElementMutation.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ElementMutation, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ElementMutation.getDescriptor =
+    sketchology.proto.ElementMutation.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ElementIdList.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ElementIdList.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ElementIdList',
+        fullName: 'sketchology.proto.ElementIdList'
+      },
+      1: {
+        name: 'uuid',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      }
+    };
+    sketchology.proto.ElementIdList.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ElementIdList, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ElementIdList.getDescriptor =
+    sketchology.proto.ElementIdList.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.Point.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.Point.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'Point',
+        fullName: 'sketchology.proto.Point'
+      },
+      1: {
+        name: 'x',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      2: {
+        name: 'y',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      }
+    };
+    sketchology.proto.Point.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.Point, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.Point.getDescriptor =
+    sketchology.proto.Point.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ElementBundle.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ElementBundle.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ElementBundle',
+        fullName: 'sketchology.proto.ElementBundle'
+      },
+      1: {
+        name: 'uuid',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      2: {
+        name: 'element',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Element
+      },
+      3: {
+        name: 'transform',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.AffineTransform
+      },
+      4: {
+        name: 'uncompressed_element',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.UncompressedElement
+      }
+    };
+    sketchology.proto.ElementBundle.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ElementBundle, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ElementBundle.getDescriptor =
+    sketchology.proto.ElementBundle.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.Path.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.Path.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'Path',
+        fullName: 'sketchology.proto.Path'
+      },
+      1: {
+        name: 'segment_types',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: sketchology.proto.Path.SegmentType.UNKNOWN,
+        type: sketchology.proto.Path.SegmentType
+      },
+      2: {
+        name: 'segment_counts',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      },
+      3: {
+        name: 'segment_args',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.DOUBLE,
+        type: Number
+      },
+      4: {
+        name: 'radius',
+        fieldType: goog.proto2.Message.FieldType.DOUBLE,
+        defaultValue: 1,
+        type: Number
+      },
+      5: {
+        name: 'rgba',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      },
+      6: {
+        name: 'end_cap',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: sketchology.proto.Path.EndCapType.ROUND,
+        type: sketchology.proto.Path.EndCapType
+      },
+      7: {
+        name: 'fill_rgba',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      }
+    };
+    sketchology.proto.Path.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.Path, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.Path.getDescriptor =
+    sketchology.proto.Path.prototype.getDescriptor;
diff --git a/third_party/ink/sketchology/proto/rect_bounds.pb.js b/third_party/ink/sketchology/proto/rect_bounds.pb.js
new file mode 100644
index 0000000..ef41a1d1
--- /dev/null
+++ b/third_party/ink/sketchology/proto/rect_bounds.pb.js
@@ -0,0 +1,525 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// Protocol Buffer 2 Copyright 2008 Google Inc.
+// All other code copyright its respective owners.
+
+/**
+ * @fileoverview Generated Protocol Buffer code for file
+ * third_party/sketchology/proto/rect_bounds.proto.
+ * Generated by //net/proto2/compiler/public:protocol_compiler.
+ * @suppress {messageConventions} 
+ */
+
+goog.provide('sketchology.proto.Rect');
+goog.provide('sketchology.proto.RectBounds');
+
+goog.require('goog.proto2.Message');
+
+
+
+/**
+ * Message Rect.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.Rect = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.Rect, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.Rect.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.Rect} The cloned message.
+ * @override
+ */
+sketchology.proto.Rect.prototype.clone;
+
+
+/**
+ * Gets the value of the xlow field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Rect.prototype.getXlow = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the xlow field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Rect.prototype.getXlowOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the xlow field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Rect.prototype.setXlow = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the xlow field has a value.
+ */
+sketchology.proto.Rect.prototype.hasXlow = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the xlow field.
+ */
+sketchology.proto.Rect.prototype.xlowCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the xlow field.
+ */
+sketchology.proto.Rect.prototype.clearXlow = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the xhigh field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Rect.prototype.getXhigh = function() {
+  return /** @type {?number} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the xhigh field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Rect.prototype.getXhighOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the xhigh field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Rect.prototype.setXhigh = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the xhigh field has a value.
+ */
+sketchology.proto.Rect.prototype.hasXhigh = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the xhigh field.
+ */
+sketchology.proto.Rect.prototype.xhighCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the xhigh field.
+ */
+sketchology.proto.Rect.prototype.clearXhigh = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the ylow field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Rect.prototype.getYlow = function() {
+  return /** @type {?number} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the ylow field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Rect.prototype.getYlowOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the ylow field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Rect.prototype.setYlow = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the ylow field has a value.
+ */
+sketchology.proto.Rect.prototype.hasYlow = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the ylow field.
+ */
+sketchology.proto.Rect.prototype.ylowCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the ylow field.
+ */
+sketchology.proto.Rect.prototype.clearYlow = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the yhigh field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Rect.prototype.getYhigh = function() {
+  return /** @type {?number} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the yhigh field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Rect.prototype.getYhighOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the yhigh field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Rect.prototype.setYhigh = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the yhigh field has a value.
+ */
+sketchology.proto.Rect.prototype.hasYhigh = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the yhigh field.
+ */
+sketchology.proto.Rect.prototype.yhighCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the yhigh field.
+ */
+sketchology.proto.Rect.prototype.clearYhigh = function() {
+  this.clear$Field(4);
+};
+
+
+
+/**
+ * Message RectBounds.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.RectBounds = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.RectBounds, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.RectBounds.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.RectBounds} The cloned message.
+ * @override
+ */
+sketchology.proto.RectBounds.prototype.clone;
+
+
+/**
+ * Gets the value of the mbr field.
+ * @return {?sketchology.proto.Rect} The value.
+ */
+sketchology.proto.RectBounds.prototype.getMbr = function() {
+  return /** @type {?sketchology.proto.Rect} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the mbr field or the default value if not set.
+ * @return {!sketchology.proto.Rect} The value.
+ */
+sketchology.proto.RectBounds.prototype.getMbrOrDefault = function() {
+  return /** @type {!sketchology.proto.Rect} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the mbr field.
+ * @param {!sketchology.proto.Rect} value The value.
+ */
+sketchology.proto.RectBounds.prototype.setMbr = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the mbr field has a value.
+ */
+sketchology.proto.RectBounds.prototype.hasMbr = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the mbr field.
+ */
+sketchology.proto.RectBounds.prototype.mbrCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the mbr field.
+ */
+sketchology.proto.RectBounds.prototype.clearMbr = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the fit_rects field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.Rect} The value.
+ */
+sketchology.proto.RectBounds.prototype.getFitRects = function(index) {
+  return /** @type {?sketchology.proto.Rect} */ (this.get$Value(2, index));
+};
+
+
+/**
+ * Gets the value of the fit_rects field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.Rect} The value.
+ */
+sketchology.proto.RectBounds.prototype.getFitRectsOrDefault = function(index) {
+  return /** @type {!sketchology.proto.Rect} */ (this.get$ValueOrDefault(2, index));
+};
+
+
+/**
+ * Adds a value to the fit_rects field.
+ * @param {!sketchology.proto.Rect} value The value to add.
+ */
+sketchology.proto.RectBounds.prototype.addFitRects = function(value) {
+  this.add$Value(2, value);
+};
+
+
+/**
+ * Returns the array of values in the fit_rects field.
+ * @return {!Array<!sketchology.proto.Rect>} The values in the field.
+ */
+sketchology.proto.RectBounds.prototype.fitRectsArray = function() {
+  return /** @type {!Array<!sketchology.proto.Rect>} */ (this.array$Values(2));
+};
+
+
+/**
+ * @return {boolean} Whether the fit_rects field has a value.
+ */
+sketchology.proto.RectBounds.prototype.hasFitRects = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the fit_rects field.
+ */
+sketchology.proto.RectBounds.prototype.fitRectsCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the fit_rects field.
+ */
+sketchology.proto.RectBounds.prototype.clearFitRects = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the max_dim field.
+ * @return {?number} The value.
+ */
+sketchology.proto.RectBounds.prototype.getMaxDim = function() {
+  return /** @type {?number} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the max_dim field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.RectBounds.prototype.getMaxDimOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the max_dim field.
+ * @param {number} value The value.
+ */
+sketchology.proto.RectBounds.prototype.setMaxDim = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the max_dim field has a value.
+ */
+sketchology.proto.RectBounds.prototype.hasMaxDim = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the max_dim field.
+ */
+sketchology.proto.RectBounds.prototype.maxDimCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the max_dim field.
+ */
+sketchology.proto.RectBounds.prototype.clearMaxDim = function() {
+  this.clear$Field(3);
+};
+
+
+/** @override */
+sketchology.proto.Rect.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.Rect.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'Rect',
+        fullName: 'sketchology.proto.Rect'
+      },
+      1: {
+        name: 'xlow',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      2: {
+        name: 'xhigh',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      3: {
+        name: 'ylow',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      4: {
+        name: 'yhigh',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      }
+    };
+    sketchology.proto.Rect.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.Rect, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.Rect.getDescriptor =
+    sketchology.proto.Rect.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.RectBounds.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.RectBounds.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'RectBounds',
+        fullName: 'sketchology.proto.RectBounds'
+      },
+      1: {
+        name: 'mbr',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Rect
+      },
+      2: {
+        name: 'fit_rects',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Rect
+      },
+      3: {
+        name: 'max_dim',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      }
+    };
+    sketchology.proto.RectBounds.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.RectBounds, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.RectBounds.getDescriptor =
+    sketchology.proto.RectBounds.prototype.getDescriptor;
diff --git a/third_party/ink/sketchology/proto/sengine.pb.js b/third_party/ink/sketchology/proto/sengine.pb.js
new file mode 100644
index 0000000..56bd87d
--- /dev/null
+++ b/third_party/ink/sketchology/proto/sengine.pb.js
@@ -0,0 +1,8196 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// Protocol Buffer 2 Copyright 2008 Google Inc.
+// All other code copyright its respective owners.
+
+/**
+ * @fileoverview Generated Protocol Buffer code for file
+ * third_party/sketchology/proto/sengine.proto.
+ * Generated by //net/proto2/compiler/public:protocol_compiler.
+ * @suppress {messageConventions} 
+ */
+
+goog.provide('sketchology.proto.Command');
+goog.provide('sketchology.proto.CommandList');
+goog.provide('sketchology.proto.NoArgCommand');
+goog.provide('sketchology.proto.ReplaceElementsCommand');
+goog.provide('sketchology.proto.EvictImageData');
+goog.provide('sketchology.proto.Viewport');
+goog.provide('sketchology.proto.ImageExport');
+goog.provide('sketchology.proto.LinearPathAnimation');
+goog.provide('sketchology.proto.LineSize');
+goog.provide('sketchology.proto.LineSize.SizeType');
+goog.provide('sketchology.proto.PusherToolParams');
+goog.provide('sketchology.proto.ToolParams');
+goog.provide('sketchology.proto.ToolParams.ToolType');
+goog.provide('sketchology.proto.FlagAssignment');
+goog.provide('sketchology.proto.AddElement');
+goog.provide('sketchology.proto.OutOfBoundsColor');
+goog.provide('sketchology.proto.SInputStream');
+goog.provide('sketchology.proto.SInput');
+goog.provide('sketchology.proto.SInput.InputType');
+goog.provide('sketchology.proto.SimulatedInput');
+goog.provide('sketchology.proto.SequencePoint');
+goog.provide('sketchology.proto.SetCallbackFlags');
+goog.provide('sketchology.proto.EngineState');
+goog.provide('sketchology.proto.CameraBoundsConfig');
+goog.provide('sketchology.proto.ImageInfo');
+goog.provide('sketchology.proto.ImageInfo.AssetType');
+goog.provide('sketchology.proto.ImageRect');
+goog.provide('sketchology.proto.GridInfo');
+goog.provide('sketchology.proto.CreateDocument');
+goog.provide('sketchology.proto.AddPath');
+goog.provide('sketchology.proto.PusherPositionUpdate');
+goog.provide('sketchology.proto.ElementQueryData');
+goog.provide('sketchology.proto.ElementQueryItem');
+goog.provide('sketchology.proto.SelectionState');
+goog.provide('sketchology.proto.ToolEvent');
+goog.provide('sketchology.proto.RenderingStrategy');
+goog.provide('sketchology.proto.BrushType');
+goog.provide('sketchology.proto.Flag');
+goog.provide('sketchology.proto.DocumentType');
+goog.provide('sketchology.proto.StorageType');
+
+goog.require('goog.proto2.Message');
+goog.require('sketchology.proto.BackgroundColor');
+goog.require('sketchology.proto.BackgroundImageInfo');
+goog.require('sketchology.proto.Border');
+goog.require('sketchology.proto.CallbackFlags');
+goog.require('sketchology.proto.ElementAnimation');
+goog.require('sketchology.proto.ElementAttributes');
+goog.require('sketchology.proto.ElementBundle');
+goog.require('sketchology.proto.ElementMutation');
+goog.require('sketchology.proto.Path');
+goog.require('sketchology.proto.Point');
+goog.require('sketchology.proto.Rect');
+goog.require('sketchology.proto.Snapshot');
+goog.require('sketchology.proto.SourceDetails');
+
+
+/**
+ * Enumeration RenderingStrategy.
+ * @enum {number}
+ */
+sketchology.proto.RenderingStrategy = {
+  UNKNOWN_RENDERER: 0,
+  BUFFERED_RENDERER: 1,
+  DIRECT_RENDERER: 2
+};
+
+
+/**
+ * Enumeration BrushType.
+ * @enum {number}
+ */
+sketchology.proto.BrushType = {
+  UNKNOWN_BRUSH: 0,
+  CALLIGRAPHY: 1,
+  INKPEN: 2,
+  MARKER: 3,
+  BALLPOINT: 4,
+  PENCIL: 5,
+  ERASER: 6,
+  AIRBRUSH: 7,
+  HIGHLIGHTER: 8,
+  GRADIENT: 9,
+  CHISEL: 10,
+  BALLPOINT_IN_PEN_MODE_ELSE_MARKER: 11
+};
+
+
+/**
+ * Enumeration Flag.
+ * @enum {number}
+ */
+sketchology.proto.Flag = {
+  UNKNOWN: 0,
+  READ_ONLY_MODE: 1,
+  ENABLE_PAN_ZOOM: 2,
+  ENABLE_ROTATION: 3,
+  ENABLE_AUTO_PEN_MODE: 4,
+  ENABLE_PEN_MODE: 5,
+  LOW_MEMORY_MODE: 6,
+  OPAQUE_PREDICTED_SEGMENT: 7
+};
+
+
+/**
+ * Enumeration DocumentType.
+ * @enum {number}
+ */
+sketchology.proto.DocumentType = {
+  UNKNOWN_DOCUMENT_TYPE: 0,
+  SINGLE_USER_DOCUMENT: 1,
+  PASSTHROUGH_DOCUMENT: 2
+};
+
+
+/**
+ * Enumeration StorageType.
+ * @enum {number}
+ */
+sketchology.proto.StorageType = {
+  UNKNOWN_STORAGE_TYPE: 0,
+  IN_MEMORY_STORAGE: 1,
+  SQLITE_STORAGE: 2
+};
+
+
+
+/**
+ * Message Command.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.Command = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.Command, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.Command.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.Command} The cloned message.
+ * @override
+ */
+sketchology.proto.Command.prototype.clone;
+
+
+/**
+ * Gets the value of the set_viewport field.
+ * @return {?sketchology.proto.Viewport} The value.
+ */
+sketchology.proto.Command.prototype.getSetViewport = function() {
+  return /** @type {?sketchology.proto.Viewport} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the set_viewport field or the default value if not set.
+ * @return {!sketchology.proto.Viewport} The value.
+ */
+sketchology.proto.Command.prototype.getSetViewportOrDefault = function() {
+  return /** @type {!sketchology.proto.Viewport} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the set_viewport field.
+ * @param {!sketchology.proto.Viewport} value The value.
+ */
+sketchology.proto.Command.prototype.setSetViewport = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the set_viewport field has a value.
+ */
+sketchology.proto.Command.prototype.hasSetViewport = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the set_viewport field.
+ */
+sketchology.proto.Command.prototype.setViewportCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the set_viewport field.
+ */
+sketchology.proto.Command.prototype.clearSetViewport = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the tool_params field.
+ * @return {?sketchology.proto.ToolParams} The value.
+ */
+sketchology.proto.Command.prototype.getToolParams = function() {
+  return /** @type {?sketchology.proto.ToolParams} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the tool_params field or the default value if not set.
+ * @return {!sketchology.proto.ToolParams} The value.
+ */
+sketchology.proto.Command.prototype.getToolParamsOrDefault = function() {
+  return /** @type {!sketchology.proto.ToolParams} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the tool_params field.
+ * @param {!sketchology.proto.ToolParams} value The value.
+ */
+sketchology.proto.Command.prototype.setToolParams = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the tool_params field has a value.
+ */
+sketchology.proto.Command.prototype.hasToolParams = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the tool_params field.
+ */
+sketchology.proto.Command.prototype.toolParamsCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the tool_params field.
+ */
+sketchology.proto.Command.prototype.clearToolParams = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the add_path field.
+ * @return {?sketchology.proto.AddPath} The value.
+ */
+sketchology.proto.Command.prototype.getAddPath = function() {
+  return /** @type {?sketchology.proto.AddPath} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the add_path field or the default value if not set.
+ * @return {!sketchology.proto.AddPath} The value.
+ */
+sketchology.proto.Command.prototype.getAddPathOrDefault = function() {
+  return /** @type {!sketchology.proto.AddPath} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the add_path field.
+ * @param {!sketchology.proto.AddPath} value The value.
+ */
+sketchology.proto.Command.prototype.setAddPath = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the add_path field has a value.
+ */
+sketchology.proto.Command.prototype.hasAddPath = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the add_path field.
+ */
+sketchology.proto.Command.prototype.addPathCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the add_path field.
+ */
+sketchology.proto.Command.prototype.clearAddPath = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the camera_position field.
+ * @return {?sketchology.proto.Rect} The value.
+ */
+sketchology.proto.Command.prototype.getCameraPosition = function() {
+  return /** @type {?sketchology.proto.Rect} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the camera_position field or the default value if not set.
+ * @return {!sketchology.proto.Rect} The value.
+ */
+sketchology.proto.Command.prototype.getCameraPositionOrDefault = function() {
+  return /** @type {!sketchology.proto.Rect} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the camera_position field.
+ * @param {!sketchology.proto.Rect} value The value.
+ */
+sketchology.proto.Command.prototype.setCameraPosition = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the camera_position field has a value.
+ */
+sketchology.proto.Command.prototype.hasCameraPosition = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the camera_position field.
+ */
+sketchology.proto.Command.prototype.cameraPositionCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the camera_position field.
+ */
+sketchology.proto.Command.prototype.clearCameraPosition = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Gets the value of the page_bounds field.
+ * @return {?sketchology.proto.Rect} The value.
+ */
+sketchology.proto.Command.prototype.getPageBounds = function() {
+  return /** @type {?sketchology.proto.Rect} */ (this.get$Value(5));
+};
+
+
+/**
+ * Gets the value of the page_bounds field or the default value if not set.
+ * @return {!sketchology.proto.Rect} The value.
+ */
+sketchology.proto.Command.prototype.getPageBoundsOrDefault = function() {
+  return /** @type {!sketchology.proto.Rect} */ (this.get$ValueOrDefault(5));
+};
+
+
+/**
+ * Sets the value of the page_bounds field.
+ * @param {!sketchology.proto.Rect} value The value.
+ */
+sketchology.proto.Command.prototype.setPageBounds = function(value) {
+  this.set$Value(5, value);
+};
+
+
+/**
+ * @return {boolean} Whether the page_bounds field has a value.
+ */
+sketchology.proto.Command.prototype.hasPageBounds = function() {
+  return this.has$Value(5);
+};
+
+
+/**
+ * @return {number} The number of values in the page_bounds field.
+ */
+sketchology.proto.Command.prototype.pageBoundsCount = function() {
+  return this.count$Values(5);
+};
+
+
+/**
+ * Clears the values in the page_bounds field.
+ */
+sketchology.proto.Command.prototype.clearPageBounds = function() {
+  this.clear$Field(5);
+};
+
+
+/**
+ * Gets the value of the image_export field.
+ * @return {?sketchology.proto.ImageExport} The value.
+ */
+sketchology.proto.Command.prototype.getImageExport = function() {
+  return /** @type {?sketchology.proto.ImageExport} */ (this.get$Value(6));
+};
+
+
+/**
+ * Gets the value of the image_export field or the default value if not set.
+ * @return {!sketchology.proto.ImageExport} The value.
+ */
+sketchology.proto.Command.prototype.getImageExportOrDefault = function() {
+  return /** @type {!sketchology.proto.ImageExport} */ (this.get$ValueOrDefault(6));
+};
+
+
+/**
+ * Sets the value of the image_export field.
+ * @param {!sketchology.proto.ImageExport} value The value.
+ */
+sketchology.proto.Command.prototype.setImageExport = function(value) {
+  this.set$Value(6, value);
+};
+
+
+/**
+ * @return {boolean} Whether the image_export field has a value.
+ */
+sketchology.proto.Command.prototype.hasImageExport = function() {
+  return this.has$Value(6);
+};
+
+
+/**
+ * @return {number} The number of values in the image_export field.
+ */
+sketchology.proto.Command.prototype.imageExportCount = function() {
+  return this.count$Values(6);
+};
+
+
+/**
+ * Clears the values in the image_export field.
+ */
+sketchology.proto.Command.prototype.clearImageExport = function() {
+  this.clear$Field(6);
+};
+
+
+/**
+ * Gets the value of the flag_assignment field.
+ * @return {?sketchology.proto.FlagAssignment} The value.
+ */
+sketchology.proto.Command.prototype.getFlagAssignment = function() {
+  return /** @type {?sketchology.proto.FlagAssignment} */ (this.get$Value(7));
+};
+
+
+/**
+ * Gets the value of the flag_assignment field or the default value if not set.
+ * @return {!sketchology.proto.FlagAssignment} The value.
+ */
+sketchology.proto.Command.prototype.getFlagAssignmentOrDefault = function() {
+  return /** @type {!sketchology.proto.FlagAssignment} */ (this.get$ValueOrDefault(7));
+};
+
+
+/**
+ * Sets the value of the flag_assignment field.
+ * @param {!sketchology.proto.FlagAssignment} value The value.
+ */
+sketchology.proto.Command.prototype.setFlagAssignment = function(value) {
+  this.set$Value(7, value);
+};
+
+
+/**
+ * @return {boolean} Whether the flag_assignment field has a value.
+ */
+sketchology.proto.Command.prototype.hasFlagAssignment = function() {
+  return this.has$Value(7);
+};
+
+
+/**
+ * @return {number} The number of values in the flag_assignment field.
+ */
+sketchology.proto.Command.prototype.flagAssignmentCount = function() {
+  return this.count$Values(7);
+};
+
+
+/**
+ * Clears the values in the flag_assignment field.
+ */
+sketchology.proto.Command.prototype.clearFlagAssignment = function() {
+  this.clear$Field(7);
+};
+
+
+/**
+ * Gets the value of the set_element_transforms field.
+ * @return {?sketchology.proto.ElementMutation} The value.
+ */
+sketchology.proto.Command.prototype.getSetElementTransforms = function() {
+  return /** @type {?sketchology.proto.ElementMutation} */ (this.get$Value(8));
+};
+
+
+/**
+ * Gets the value of the set_element_transforms field or the default value if not set.
+ * @return {!sketchology.proto.ElementMutation} The value.
+ */
+sketchology.proto.Command.prototype.getSetElementTransformsOrDefault = function() {
+  return /** @type {!sketchology.proto.ElementMutation} */ (this.get$ValueOrDefault(8));
+};
+
+
+/**
+ * Sets the value of the set_element_transforms field.
+ * @param {!sketchology.proto.ElementMutation} value The value.
+ */
+sketchology.proto.Command.prototype.setSetElementTransforms = function(value) {
+  this.set$Value(8, value);
+};
+
+
+/**
+ * @return {boolean} Whether the set_element_transforms field has a value.
+ */
+sketchology.proto.Command.prototype.hasSetElementTransforms = function() {
+  return this.has$Value(8);
+};
+
+
+/**
+ * @return {number} The number of values in the set_element_transforms field.
+ */
+sketchology.proto.Command.prototype.setElementTransformsCount = function() {
+  return this.count$Values(8);
+};
+
+
+/**
+ * Clears the values in the set_element_transforms field.
+ */
+sketchology.proto.Command.prototype.clearSetElementTransforms = function() {
+  this.clear$Field(8);
+};
+
+
+/**
+ * Gets the value of the add_element field.
+ * @return {?sketchology.proto.AddElement} The value.
+ */
+sketchology.proto.Command.prototype.getAddElement = function() {
+  return /** @type {?sketchology.proto.AddElement} */ (this.get$Value(9));
+};
+
+
+/**
+ * Gets the value of the add_element field or the default value if not set.
+ * @return {!sketchology.proto.AddElement} The value.
+ */
+sketchology.proto.Command.prototype.getAddElementOrDefault = function() {
+  return /** @type {!sketchology.proto.AddElement} */ (this.get$ValueOrDefault(9));
+};
+
+
+/**
+ * Sets the value of the add_element field.
+ * @param {!sketchology.proto.AddElement} value The value.
+ */
+sketchology.proto.Command.prototype.setAddElement = function(value) {
+  this.set$Value(9, value);
+};
+
+
+/**
+ * @return {boolean} Whether the add_element field has a value.
+ */
+sketchology.proto.Command.prototype.hasAddElement = function() {
+  return this.has$Value(9);
+};
+
+
+/**
+ * @return {number} The number of values in the add_element field.
+ */
+sketchology.proto.Command.prototype.addElementCount = function() {
+  return this.count$Values(9);
+};
+
+
+/**
+ * Clears the values in the add_element field.
+ */
+sketchology.proto.Command.prototype.clearAddElement = function() {
+  this.clear$Field(9);
+};
+
+
+/**
+ * Gets the value of the background_image field.
+ * @return {?sketchology.proto.BackgroundImageInfo} The value.
+ */
+sketchology.proto.Command.prototype.getBackgroundImage = function() {
+  return /** @type {?sketchology.proto.BackgroundImageInfo} */ (this.get$Value(10));
+};
+
+
+/**
+ * Gets the value of the background_image field or the default value if not set.
+ * @return {!sketchology.proto.BackgroundImageInfo} The value.
+ */
+sketchology.proto.Command.prototype.getBackgroundImageOrDefault = function() {
+  return /** @type {!sketchology.proto.BackgroundImageInfo} */ (this.get$ValueOrDefault(10));
+};
+
+
+/**
+ * Sets the value of the background_image field.
+ * @param {!sketchology.proto.BackgroundImageInfo} value The value.
+ */
+sketchology.proto.Command.prototype.setBackgroundImage = function(value) {
+  this.set$Value(10, value);
+};
+
+
+/**
+ * @return {boolean} Whether the background_image field has a value.
+ */
+sketchology.proto.Command.prototype.hasBackgroundImage = function() {
+  return this.has$Value(10);
+};
+
+
+/**
+ * @return {number} The number of values in the background_image field.
+ */
+sketchology.proto.Command.prototype.backgroundImageCount = function() {
+  return this.count$Values(10);
+};
+
+
+/**
+ * Clears the values in the background_image field.
+ */
+sketchology.proto.Command.prototype.clearBackgroundImage = function() {
+  this.clear$Field(10);
+};
+
+
+/**
+ * Gets the value of the background_color field.
+ * @return {?sketchology.proto.BackgroundColor} The value.
+ */
+sketchology.proto.Command.prototype.getBackgroundColor = function() {
+  return /** @type {?sketchology.proto.BackgroundColor} */ (this.get$Value(11));
+};
+
+
+/**
+ * Gets the value of the background_color field or the default value if not set.
+ * @return {!sketchology.proto.BackgroundColor} The value.
+ */
+sketchology.proto.Command.prototype.getBackgroundColorOrDefault = function() {
+  return /** @type {!sketchology.proto.BackgroundColor} */ (this.get$ValueOrDefault(11));
+};
+
+
+/**
+ * Sets the value of the background_color field.
+ * @param {!sketchology.proto.BackgroundColor} value The value.
+ */
+sketchology.proto.Command.prototype.setBackgroundColor = function(value) {
+  this.set$Value(11, value);
+};
+
+
+/**
+ * @return {boolean} Whether the background_color field has a value.
+ */
+sketchology.proto.Command.prototype.hasBackgroundColor = function() {
+  return this.has$Value(11);
+};
+
+
+/**
+ * @return {number} The number of values in the background_color field.
+ */
+sketchology.proto.Command.prototype.backgroundColorCount = function() {
+  return this.count$Values(11);
+};
+
+
+/**
+ * Clears the values in the background_color field.
+ */
+sketchology.proto.Command.prototype.clearBackgroundColor = function() {
+  this.clear$Field(11);
+};
+
+
+/**
+ * Gets the value of the set_out_of_bounds_color field.
+ * @return {?sketchology.proto.OutOfBoundsColor} The value.
+ */
+sketchology.proto.Command.prototype.getSetOutOfBoundsColor = function() {
+  return /** @type {?sketchology.proto.OutOfBoundsColor} */ (this.get$Value(12));
+};
+
+
+/**
+ * Gets the value of the set_out_of_bounds_color field or the default value if not set.
+ * @return {!sketchology.proto.OutOfBoundsColor} The value.
+ */
+sketchology.proto.Command.prototype.getSetOutOfBoundsColorOrDefault = function() {
+  return /** @type {!sketchology.proto.OutOfBoundsColor} */ (this.get$ValueOrDefault(12));
+};
+
+
+/**
+ * Sets the value of the set_out_of_bounds_color field.
+ * @param {!sketchology.proto.OutOfBoundsColor} value The value.
+ */
+sketchology.proto.Command.prototype.setSetOutOfBoundsColor = function(value) {
+  this.set$Value(12, value);
+};
+
+
+/**
+ * @return {boolean} Whether the set_out_of_bounds_color field has a value.
+ */
+sketchology.proto.Command.prototype.hasSetOutOfBoundsColor = function() {
+  return this.has$Value(12);
+};
+
+
+/**
+ * @return {number} The number of values in the set_out_of_bounds_color field.
+ */
+sketchology.proto.Command.prototype.setOutOfBoundsColorCount = function() {
+  return this.count$Values(12);
+};
+
+
+/**
+ * Clears the values in the set_out_of_bounds_color field.
+ */
+sketchology.proto.Command.prototype.clearSetOutOfBoundsColor = function() {
+  this.clear$Field(12);
+};
+
+
+/**
+ * Gets the value of the set_page_border field.
+ * @return {?sketchology.proto.Border} The value.
+ */
+sketchology.proto.Command.prototype.getSetPageBorder = function() {
+  return /** @type {?sketchology.proto.Border} */ (this.get$Value(13));
+};
+
+
+/**
+ * Gets the value of the set_page_border field or the default value if not set.
+ * @return {!sketchology.proto.Border} The value.
+ */
+sketchology.proto.Command.prototype.getSetPageBorderOrDefault = function() {
+  return /** @type {!sketchology.proto.Border} */ (this.get$ValueOrDefault(13));
+};
+
+
+/**
+ * Sets the value of the set_page_border field.
+ * @param {!sketchology.proto.Border} value The value.
+ */
+sketchology.proto.Command.prototype.setSetPageBorder = function(value) {
+  this.set$Value(13, value);
+};
+
+
+/**
+ * @return {boolean} Whether the set_page_border field has a value.
+ */
+sketchology.proto.Command.prototype.hasSetPageBorder = function() {
+  return this.has$Value(13);
+};
+
+
+/**
+ * @return {number} The number of values in the set_page_border field.
+ */
+sketchology.proto.Command.prototype.setPageBorderCount = function() {
+  return this.count$Values(13);
+};
+
+
+/**
+ * Clears the values in the set_page_border field.
+ */
+sketchology.proto.Command.prototype.clearSetPageBorder = function() {
+  this.clear$Field(13);
+};
+
+
+/**
+ * Gets the value of the send_input_stream field.
+ * @return {?sketchology.proto.SInputStream} The value.
+ */
+sketchology.proto.Command.prototype.getSendInputStream = function() {
+  return /** @type {?sketchology.proto.SInputStream} */ (this.get$Value(14));
+};
+
+
+/**
+ * Gets the value of the send_input_stream field or the default value if not set.
+ * @return {!sketchology.proto.SInputStream} The value.
+ */
+sketchology.proto.Command.prototype.getSendInputStreamOrDefault = function() {
+  return /** @type {!sketchology.proto.SInputStream} */ (this.get$ValueOrDefault(14));
+};
+
+
+/**
+ * Sets the value of the send_input_stream field.
+ * @param {!sketchology.proto.SInputStream} value The value.
+ */
+sketchology.proto.Command.prototype.setSendInputStream = function(value) {
+  this.set$Value(14, value);
+};
+
+
+/**
+ * @return {boolean} Whether the send_input_stream field has a value.
+ */
+sketchology.proto.Command.prototype.hasSendInputStream = function() {
+  return this.has$Value(14);
+};
+
+
+/**
+ * @return {number} The number of values in the send_input_stream field.
+ */
+sketchology.proto.Command.prototype.sendInputStreamCount = function() {
+  return this.count$Values(14);
+};
+
+
+/**
+ * Clears the values in the send_input_stream field.
+ */
+sketchology.proto.Command.prototype.clearSendInputStream = function() {
+  this.clear$Field(14);
+};
+
+
+/**
+ * Gets the value of the sequence_point field.
+ * @return {?sketchology.proto.SequencePoint} The value.
+ */
+sketchology.proto.Command.prototype.getSequencePoint = function() {
+  return /** @type {?sketchology.proto.SequencePoint} */ (this.get$Value(15));
+};
+
+
+/**
+ * Gets the value of the sequence_point field or the default value if not set.
+ * @return {!sketchology.proto.SequencePoint} The value.
+ */
+sketchology.proto.Command.prototype.getSequencePointOrDefault = function() {
+  return /** @type {!sketchology.proto.SequencePoint} */ (this.get$ValueOrDefault(15));
+};
+
+
+/**
+ * Sets the value of the sequence_point field.
+ * @param {!sketchology.proto.SequencePoint} value The value.
+ */
+sketchology.proto.Command.prototype.setSequencePoint = function(value) {
+  this.set$Value(15, value);
+};
+
+
+/**
+ * @return {boolean} Whether the sequence_point field has a value.
+ */
+sketchology.proto.Command.prototype.hasSequencePoint = function() {
+  return this.has$Value(15);
+};
+
+
+/**
+ * @return {number} The number of values in the sequence_point field.
+ */
+sketchology.proto.Command.prototype.sequencePointCount = function() {
+  return this.count$Values(15);
+};
+
+
+/**
+ * Clears the values in the sequence_point field.
+ */
+sketchology.proto.Command.prototype.clearSequencePoint = function() {
+  this.clear$Field(15);
+};
+
+
+/**
+ * Gets the value of the set_callback_flags field.
+ * @return {?sketchology.proto.SetCallbackFlags} The value.
+ */
+sketchology.proto.Command.prototype.getSetCallbackFlags = function() {
+  return /** @type {?sketchology.proto.SetCallbackFlags} */ (this.get$Value(16));
+};
+
+
+/**
+ * Gets the value of the set_callback_flags field or the default value if not set.
+ * @return {!sketchology.proto.SetCallbackFlags} The value.
+ */
+sketchology.proto.Command.prototype.getSetCallbackFlagsOrDefault = function() {
+  return /** @type {!sketchology.proto.SetCallbackFlags} */ (this.get$ValueOrDefault(16));
+};
+
+
+/**
+ * Sets the value of the set_callback_flags field.
+ * @param {!sketchology.proto.SetCallbackFlags} value The value.
+ */
+sketchology.proto.Command.prototype.setSetCallbackFlags = function(value) {
+  this.set$Value(16, value);
+};
+
+
+/**
+ * @return {boolean} Whether the set_callback_flags field has a value.
+ */
+sketchology.proto.Command.prototype.hasSetCallbackFlags = function() {
+  return this.has$Value(16);
+};
+
+
+/**
+ * @return {number} The number of values in the set_callback_flags field.
+ */
+sketchology.proto.Command.prototype.setCallbackFlagsCount = function() {
+  return this.count$Values(16);
+};
+
+
+/**
+ * Clears the values in the set_callback_flags field.
+ */
+sketchology.proto.Command.prototype.clearSetCallbackFlags = function() {
+  this.clear$Field(16);
+};
+
+
+/**
+ * Gets the value of the set_camera_bounds_config field.
+ * @return {?sketchology.proto.CameraBoundsConfig} The value.
+ */
+sketchology.proto.Command.prototype.getSetCameraBoundsConfig = function() {
+  return /** @type {?sketchology.proto.CameraBoundsConfig} */ (this.get$Value(17));
+};
+
+
+/**
+ * Gets the value of the set_camera_bounds_config field or the default value if not set.
+ * @return {!sketchology.proto.CameraBoundsConfig} The value.
+ */
+sketchology.proto.Command.prototype.getSetCameraBoundsConfigOrDefault = function() {
+  return /** @type {!sketchology.proto.CameraBoundsConfig} */ (this.get$ValueOrDefault(17));
+};
+
+
+/**
+ * Sets the value of the set_camera_bounds_config field.
+ * @param {!sketchology.proto.CameraBoundsConfig} value The value.
+ */
+sketchology.proto.Command.prototype.setSetCameraBoundsConfig = function(value) {
+  this.set$Value(17, value);
+};
+
+
+/**
+ * @return {boolean} Whether the set_camera_bounds_config field has a value.
+ */
+sketchology.proto.Command.prototype.hasSetCameraBoundsConfig = function() {
+  return this.has$Value(17);
+};
+
+
+/**
+ * @return {number} The number of values in the set_camera_bounds_config field.
+ */
+sketchology.proto.Command.prototype.setCameraBoundsConfigCount = function() {
+  return this.count$Values(17);
+};
+
+
+/**
+ * Clears the values in the set_camera_bounds_config field.
+ */
+sketchology.proto.Command.prototype.clearSetCameraBoundsConfig = function() {
+  this.clear$Field(17);
+};
+
+
+/**
+ * Gets the value of the deselect_all field.
+ * @return {?sketchology.proto.NoArgCommand} The value.
+ */
+sketchology.proto.Command.prototype.getDeselectAll = function() {
+  return /** @type {?sketchology.proto.NoArgCommand} */ (this.get$Value(18));
+};
+
+
+/**
+ * Gets the value of the deselect_all field or the default value if not set.
+ * @return {!sketchology.proto.NoArgCommand} The value.
+ */
+sketchology.proto.Command.prototype.getDeselectAllOrDefault = function() {
+  return /** @type {!sketchology.proto.NoArgCommand} */ (this.get$ValueOrDefault(18));
+};
+
+
+/**
+ * Sets the value of the deselect_all field.
+ * @param {!sketchology.proto.NoArgCommand} value The value.
+ */
+sketchology.proto.Command.prototype.setDeselectAll = function(value) {
+  this.set$Value(18, value);
+};
+
+
+/**
+ * @return {boolean} Whether the deselect_all field has a value.
+ */
+sketchology.proto.Command.prototype.hasDeselectAll = function() {
+  return this.has$Value(18);
+};
+
+
+/**
+ * @return {number} The number of values in the deselect_all field.
+ */
+sketchology.proto.Command.prototype.deselectAllCount = function() {
+  return this.count$Values(18);
+};
+
+
+/**
+ * Clears the values in the deselect_all field.
+ */
+sketchology.proto.Command.prototype.clearDeselectAll = function() {
+  this.clear$Field(18);
+};
+
+
+/**
+ * Gets the value of the add_image_rect field.
+ * @return {?sketchology.proto.ImageRect} The value.
+ */
+sketchology.proto.Command.prototype.getAddImageRect = function() {
+  return /** @type {?sketchology.proto.ImageRect} */ (this.get$Value(19));
+};
+
+
+/**
+ * Gets the value of the add_image_rect field or the default value if not set.
+ * @return {!sketchology.proto.ImageRect} The value.
+ */
+sketchology.proto.Command.prototype.getAddImageRectOrDefault = function() {
+  return /** @type {!sketchology.proto.ImageRect} */ (this.get$ValueOrDefault(19));
+};
+
+
+/**
+ * Sets the value of the add_image_rect field.
+ * @param {!sketchology.proto.ImageRect} value The value.
+ */
+sketchology.proto.Command.prototype.setAddImageRect = function(value) {
+  this.set$Value(19, value);
+};
+
+
+/**
+ * @return {boolean} Whether the add_image_rect field has a value.
+ */
+sketchology.proto.Command.prototype.hasAddImageRect = function() {
+  return this.has$Value(19);
+};
+
+
+/**
+ * @return {number} The number of values in the add_image_rect field.
+ */
+sketchology.proto.Command.prototype.addImageRectCount = function() {
+  return this.count$Values(19);
+};
+
+
+/**
+ * Clears the values in the add_image_rect field.
+ */
+sketchology.proto.Command.prototype.clearAddImageRect = function() {
+  this.clear$Field(19);
+};
+
+
+/**
+ * Gets the value of the clear field.
+ * @return {?sketchology.proto.NoArgCommand} The value.
+ */
+sketchology.proto.Command.prototype.getClear = function() {
+  return /** @type {?sketchology.proto.NoArgCommand} */ (this.get$Value(21));
+};
+
+
+/**
+ * Gets the value of the clear field or the default value if not set.
+ * @return {!sketchology.proto.NoArgCommand} The value.
+ */
+sketchology.proto.Command.prototype.getClearOrDefault = function() {
+  return /** @type {!sketchology.proto.NoArgCommand} */ (this.get$ValueOrDefault(21));
+};
+
+
+/**
+ * Sets the value of the clear field.
+ * @param {!sketchology.proto.NoArgCommand} value The value.
+ */
+sketchology.proto.Command.prototype.setClear = function(value) {
+  this.set$Value(21, value);
+};
+
+
+/**
+ * @return {boolean} Whether the clear field has a value.
+ */
+sketchology.proto.Command.prototype.hasClear = function() {
+  return this.has$Value(21);
+};
+
+
+/**
+ * @return {number} The number of values in the clear field.
+ */
+sketchology.proto.Command.prototype.clearCount = function() {
+  return this.count$Values(21);
+};
+
+
+/**
+ * Clears the values in the clear field.
+ */
+sketchology.proto.Command.prototype.clearClear = function() {
+  this.clear$Field(21);
+};
+
+
+/**
+ * Gets the value of the remove_all_elements field.
+ * @return {?sketchology.proto.NoArgCommand} The value.
+ */
+sketchology.proto.Command.prototype.getRemoveAllElements = function() {
+  return /** @type {?sketchology.proto.NoArgCommand} */ (this.get$Value(22));
+};
+
+
+/**
+ * Gets the value of the remove_all_elements field or the default value if not set.
+ * @return {!sketchology.proto.NoArgCommand} The value.
+ */
+sketchology.proto.Command.prototype.getRemoveAllElementsOrDefault = function() {
+  return /** @type {!sketchology.proto.NoArgCommand} */ (this.get$ValueOrDefault(22));
+};
+
+
+/**
+ * Sets the value of the remove_all_elements field.
+ * @param {!sketchology.proto.NoArgCommand} value The value.
+ */
+sketchology.proto.Command.prototype.setRemoveAllElements = function(value) {
+  this.set$Value(22, value);
+};
+
+
+/**
+ * @return {boolean} Whether the remove_all_elements field has a value.
+ */
+sketchology.proto.Command.prototype.hasRemoveAllElements = function() {
+  return this.has$Value(22);
+};
+
+
+/**
+ * @return {number} The number of values in the remove_all_elements field.
+ */
+sketchology.proto.Command.prototype.removeAllElementsCount = function() {
+  return this.count$Values(22);
+};
+
+
+/**
+ * Clears the values in the remove_all_elements field.
+ */
+sketchology.proto.Command.prototype.clearRemoveAllElements = function() {
+  this.clear$Field(22);
+};
+
+
+/**
+ * Gets the value of the undo field.
+ * @return {?sketchology.proto.NoArgCommand} The value.
+ */
+sketchology.proto.Command.prototype.getUndo = function() {
+  return /** @type {?sketchology.proto.NoArgCommand} */ (this.get$Value(23));
+};
+
+
+/**
+ * Gets the value of the undo field or the default value if not set.
+ * @return {!sketchology.proto.NoArgCommand} The value.
+ */
+sketchology.proto.Command.prototype.getUndoOrDefault = function() {
+  return /** @type {!sketchology.proto.NoArgCommand} */ (this.get$ValueOrDefault(23));
+};
+
+
+/**
+ * Sets the value of the undo field.
+ * @param {!sketchology.proto.NoArgCommand} value The value.
+ */
+sketchology.proto.Command.prototype.setUndo = function(value) {
+  this.set$Value(23, value);
+};
+
+
+/**
+ * @return {boolean} Whether the undo field has a value.
+ */
+sketchology.proto.Command.prototype.hasUndo = function() {
+  return this.has$Value(23);
+};
+
+
+/**
+ * @return {number} The number of values in the undo field.
+ */
+sketchology.proto.Command.prototype.undoCount = function() {
+  return this.count$Values(23);
+};
+
+
+/**
+ * Clears the values in the undo field.
+ */
+sketchology.proto.Command.prototype.clearUndo = function() {
+  this.clear$Field(23);
+};
+
+
+/**
+ * Gets the value of the redo field.
+ * @return {?sketchology.proto.NoArgCommand} The value.
+ */
+sketchology.proto.Command.prototype.getRedo = function() {
+  return /** @type {?sketchology.proto.NoArgCommand} */ (this.get$Value(24));
+};
+
+
+/**
+ * Gets the value of the redo field or the default value if not set.
+ * @return {!sketchology.proto.NoArgCommand} The value.
+ */
+sketchology.proto.Command.prototype.getRedoOrDefault = function() {
+  return /** @type {!sketchology.proto.NoArgCommand} */ (this.get$ValueOrDefault(24));
+};
+
+
+/**
+ * Sets the value of the redo field.
+ * @param {!sketchology.proto.NoArgCommand} value The value.
+ */
+sketchology.proto.Command.prototype.setRedo = function(value) {
+  this.set$Value(24, value);
+};
+
+
+/**
+ * @return {boolean} Whether the redo field has a value.
+ */
+sketchology.proto.Command.prototype.hasRedo = function() {
+  return this.has$Value(24);
+};
+
+
+/**
+ * @return {number} The number of values in the redo field.
+ */
+sketchology.proto.Command.prototype.redoCount = function() {
+  return this.count$Values(24);
+};
+
+
+/**
+ * Clears the values in the redo field.
+ */
+sketchology.proto.Command.prototype.clearRedo = function() {
+  this.clear$Field(24);
+};
+
+
+/**
+ * Gets the value of the evict_image_data field.
+ * @return {?sketchology.proto.EvictImageData} The value.
+ */
+sketchology.proto.Command.prototype.getEvictImageData = function() {
+  return /** @type {?sketchology.proto.EvictImageData} */ (this.get$Value(25));
+};
+
+
+/**
+ * Gets the value of the evict_image_data field or the default value if not set.
+ * @return {!sketchology.proto.EvictImageData} The value.
+ */
+sketchology.proto.Command.prototype.getEvictImageDataOrDefault = function() {
+  return /** @type {!sketchology.proto.EvictImageData} */ (this.get$ValueOrDefault(25));
+};
+
+
+/**
+ * Sets the value of the evict_image_data field.
+ * @param {!sketchology.proto.EvictImageData} value The value.
+ */
+sketchology.proto.Command.prototype.setEvictImageData = function(value) {
+  this.set$Value(25, value);
+};
+
+
+/**
+ * @return {boolean} Whether the evict_image_data field has a value.
+ */
+sketchology.proto.Command.prototype.hasEvictImageData = function() {
+  return this.has$Value(25);
+};
+
+
+/**
+ * @return {number} The number of values in the evict_image_data field.
+ */
+sketchology.proto.Command.prototype.evictImageDataCount = function() {
+  return this.count$Values(25);
+};
+
+
+/**
+ * Clears the values in the evict_image_data field.
+ */
+sketchology.proto.Command.prototype.clearEvictImageData = function() {
+  this.clear$Field(25);
+};
+
+
+/**
+ * Gets the value of the replace_elements field.
+ * @return {?sketchology.proto.ReplaceElementsCommand} The value.
+ */
+sketchology.proto.Command.prototype.getReplaceElements = function() {
+  return /** @type {?sketchology.proto.ReplaceElementsCommand} */ (this.get$Value(26));
+};
+
+
+/**
+ * Gets the value of the replace_elements field or the default value if not set.
+ * @return {!sketchology.proto.ReplaceElementsCommand} The value.
+ */
+sketchology.proto.Command.prototype.getReplaceElementsOrDefault = function() {
+  return /** @type {!sketchology.proto.ReplaceElementsCommand} */ (this.get$ValueOrDefault(26));
+};
+
+
+/**
+ * Sets the value of the replace_elements field.
+ * @param {!sketchology.proto.ReplaceElementsCommand} value The value.
+ */
+sketchology.proto.Command.prototype.setReplaceElements = function(value) {
+  this.set$Value(26, value);
+};
+
+
+/**
+ * @return {boolean} Whether the replace_elements field has a value.
+ */
+sketchology.proto.Command.prototype.hasReplaceElements = function() {
+  return this.has$Value(26);
+};
+
+
+/**
+ * @return {number} The number of values in the replace_elements field.
+ */
+sketchology.proto.Command.prototype.replaceElementsCount = function() {
+  return this.count$Values(26);
+};
+
+
+/**
+ * Clears the values in the replace_elements field.
+ */
+sketchology.proto.Command.prototype.clearReplaceElements = function() {
+  this.clear$Field(26);
+};
+
+
+/**
+ * Gets the value of the commit_crop field.
+ * @return {?sketchology.proto.NoArgCommand} The value.
+ */
+sketchology.proto.Command.prototype.getCommitCrop = function() {
+  return /** @type {?sketchology.proto.NoArgCommand} */ (this.get$Value(27));
+};
+
+
+/**
+ * Gets the value of the commit_crop field or the default value if not set.
+ * @return {!sketchology.proto.NoArgCommand} The value.
+ */
+sketchology.proto.Command.prototype.getCommitCropOrDefault = function() {
+  return /** @type {!sketchology.proto.NoArgCommand} */ (this.get$ValueOrDefault(27));
+};
+
+
+/**
+ * Sets the value of the commit_crop field.
+ * @param {!sketchology.proto.NoArgCommand} value The value.
+ */
+sketchology.proto.Command.prototype.setCommitCrop = function(value) {
+  this.set$Value(27, value);
+};
+
+
+/**
+ * @return {boolean} Whether the commit_crop field has a value.
+ */
+sketchology.proto.Command.prototype.hasCommitCrop = function() {
+  return this.has$Value(27);
+};
+
+
+/**
+ * @return {number} The number of values in the commit_crop field.
+ */
+sketchology.proto.Command.prototype.commitCropCount = function() {
+  return this.count$Values(27);
+};
+
+
+/**
+ * Clears the values in the commit_crop field.
+ */
+sketchology.proto.Command.prototype.clearCommitCrop = function() {
+  this.clear$Field(27);
+};
+
+
+/**
+ * Gets the value of the element_animation field.
+ * @return {?sketchology.proto.ElementAnimation} The value.
+ */
+sketchology.proto.Command.prototype.getElementAnimation = function() {
+  return /** @type {?sketchology.proto.ElementAnimation} */ (this.get$Value(28));
+};
+
+
+/**
+ * Gets the value of the element_animation field or the default value if not set.
+ * @return {!sketchology.proto.ElementAnimation} The value.
+ */
+sketchology.proto.Command.prototype.getElementAnimationOrDefault = function() {
+  return /** @type {!sketchology.proto.ElementAnimation} */ (this.get$ValueOrDefault(28));
+};
+
+
+/**
+ * Sets the value of the element_animation field.
+ * @param {!sketchology.proto.ElementAnimation} value The value.
+ */
+sketchology.proto.Command.prototype.setElementAnimation = function(value) {
+  this.set$Value(28, value);
+};
+
+
+/**
+ * @return {boolean} Whether the element_animation field has a value.
+ */
+sketchology.proto.Command.prototype.hasElementAnimation = function() {
+  return this.has$Value(28);
+};
+
+
+/**
+ * @return {number} The number of values in the element_animation field.
+ */
+sketchology.proto.Command.prototype.elementAnimationCount = function() {
+  return this.count$Values(28);
+};
+
+
+/**
+ * Clears the values in the element_animation field.
+ */
+sketchology.proto.Command.prototype.clearElementAnimation = function() {
+  this.clear$Field(28);
+};
+
+
+/**
+ * Gets the value of the set_grid field.
+ * @return {?sketchology.proto.GridInfo} The value.
+ */
+sketchology.proto.Command.prototype.getSetGrid = function() {
+  return /** @type {?sketchology.proto.GridInfo} */ (this.get$Value(29));
+};
+
+
+/**
+ * Gets the value of the set_grid field or the default value if not set.
+ * @return {!sketchology.proto.GridInfo} The value.
+ */
+sketchology.proto.Command.prototype.getSetGridOrDefault = function() {
+  return /** @type {!sketchology.proto.GridInfo} */ (this.get$ValueOrDefault(29));
+};
+
+
+/**
+ * Sets the value of the set_grid field.
+ * @param {!sketchology.proto.GridInfo} value The value.
+ */
+sketchology.proto.Command.prototype.setSetGrid = function(value) {
+  this.set$Value(29, value);
+};
+
+
+/**
+ * @return {boolean} Whether the set_grid field has a value.
+ */
+sketchology.proto.Command.prototype.hasSetGrid = function() {
+  return this.has$Value(29);
+};
+
+
+/**
+ * @return {number} The number of values in the set_grid field.
+ */
+sketchology.proto.Command.prototype.setGridCount = function() {
+  return this.count$Values(29);
+};
+
+
+/**
+ * Clears the values in the set_grid field.
+ */
+sketchology.proto.Command.prototype.clearSetGrid = function() {
+  this.clear$Field(29);
+};
+
+
+/**
+ * Gets the value of the clear_grid field.
+ * @return {?sketchology.proto.NoArgCommand} The value.
+ */
+sketchology.proto.Command.prototype.getClearGrid = function() {
+  return /** @type {?sketchology.proto.NoArgCommand} */ (this.get$Value(30));
+};
+
+
+/**
+ * Gets the value of the clear_grid field or the default value if not set.
+ * @return {!sketchology.proto.NoArgCommand} The value.
+ */
+sketchology.proto.Command.prototype.getClearGridOrDefault = function() {
+  return /** @type {!sketchology.proto.NoArgCommand} */ (this.get$ValueOrDefault(30));
+};
+
+
+/**
+ * Sets the value of the clear_grid field.
+ * @param {!sketchology.proto.NoArgCommand} value The value.
+ */
+sketchology.proto.Command.prototype.setClearGrid = function(value) {
+  this.set$Value(30, value);
+};
+
+
+/**
+ * @return {boolean} Whether the clear_grid field has a value.
+ */
+sketchology.proto.Command.prototype.hasClearGrid = function() {
+  return this.has$Value(30);
+};
+
+
+/**
+ * @return {number} The number of values in the clear_grid field.
+ */
+sketchology.proto.Command.prototype.clearGridCount = function() {
+  return this.count$Values(30);
+};
+
+
+/**
+ * Clears the values in the clear_grid field.
+ */
+sketchology.proto.Command.prototype.clearClearGrid = function() {
+  this.clear$Field(30);
+};
+
+
+
+/**
+ * Message CommandList.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.CommandList = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.CommandList, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.CommandList.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.CommandList} The cloned message.
+ * @override
+ */
+sketchology.proto.CommandList.prototype.clone;
+
+
+/**
+ * Gets the value of the commands field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.Command} The value.
+ */
+sketchology.proto.CommandList.prototype.getCommands = function(index) {
+  return /** @type {?sketchology.proto.Command} */ (this.get$Value(1, index));
+};
+
+
+/**
+ * Gets the value of the commands field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.Command} The value.
+ */
+sketchology.proto.CommandList.prototype.getCommandsOrDefault = function(index) {
+  return /** @type {!sketchology.proto.Command} */ (this.get$ValueOrDefault(1, index));
+};
+
+
+/**
+ * Adds a value to the commands field.
+ * @param {!sketchology.proto.Command} value The value to add.
+ */
+sketchology.proto.CommandList.prototype.addCommands = function(value) {
+  this.add$Value(1, value);
+};
+
+
+/**
+ * Returns the array of values in the commands field.
+ * @return {!Array<!sketchology.proto.Command>} The values in the field.
+ */
+sketchology.proto.CommandList.prototype.commandsArray = function() {
+  return /** @type {!Array<!sketchology.proto.Command>} */ (this.array$Values(1));
+};
+
+
+/**
+ * @return {boolean} Whether the commands field has a value.
+ */
+sketchology.proto.CommandList.prototype.hasCommands = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the commands field.
+ */
+sketchology.proto.CommandList.prototype.commandsCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the commands field.
+ */
+sketchology.proto.CommandList.prototype.clearCommands = function() {
+  this.clear$Field(1);
+};
+
+
+
+/**
+ * Message NoArgCommand.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.NoArgCommand = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.NoArgCommand, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.NoArgCommand.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.NoArgCommand} The cloned message.
+ * @override
+ */
+sketchology.proto.NoArgCommand.prototype.clone;
+
+
+
+/**
+ * Message ReplaceElementsCommand.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ReplaceElementsCommand = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ReplaceElementsCommand, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ReplaceElementsCommand.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ReplaceElementsCommand} The cloned message.
+ * @override
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.clone;
+
+
+/**
+ * Gets the value of the uuids_to_remove field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?string} The value.
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.getUuidsToRemove = function(index) {
+  return /** @type {?string} */ (this.get$Value(1, index));
+};
+
+
+/**
+ * Gets the value of the uuids_to_remove field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {string} The value.
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.getUuidsToRemoveOrDefault = function(index) {
+  return /** @type {string} */ (this.get$ValueOrDefault(1, index));
+};
+
+
+/**
+ * Adds a value to the uuids_to_remove field.
+ * @param {string} value The value to add.
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.addUuidsToRemove = function(value) {
+  this.add$Value(1, value);
+};
+
+
+/**
+ * Returns the array of values in the uuids_to_remove field.
+ * @return {!Array<string>} The values in the field.
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.uuidsToRemoveArray = function() {
+  return /** @type {!Array<string>} */ (this.array$Values(1));
+};
+
+
+/**
+ * @return {boolean} Whether the uuids_to_remove field has a value.
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.hasUuidsToRemove = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uuids_to_remove field.
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.uuidsToRemoveCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uuids_to_remove field.
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.clearUuidsToRemove = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the paths_to_add field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.Path} The value.
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.getPathsToAdd = function(index) {
+  return /** @type {?sketchology.proto.Path} */ (this.get$Value(2, index));
+};
+
+
+/**
+ * Gets the value of the paths_to_add field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.Path} The value.
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.getPathsToAddOrDefault = function(index) {
+  return /** @type {!sketchology.proto.Path} */ (this.get$ValueOrDefault(2, index));
+};
+
+
+/**
+ * Adds a value to the paths_to_add field.
+ * @param {!sketchology.proto.Path} value The value to add.
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.addPathsToAdd = function(value) {
+  this.add$Value(2, value);
+};
+
+
+/**
+ * Returns the array of values in the paths_to_add field.
+ * @return {!Array<!sketchology.proto.Path>} The values in the field.
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.pathsToAddArray = function() {
+  return /** @type {!Array<!sketchology.proto.Path>} */ (this.array$Values(2));
+};
+
+
+/**
+ * @return {boolean} Whether the paths_to_add field has a value.
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.hasPathsToAdd = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the paths_to_add field.
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.pathsToAddCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the paths_to_add field.
+ */
+sketchology.proto.ReplaceElementsCommand.prototype.clearPathsToAdd = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message EvictImageData.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.EvictImageData = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.EvictImageData, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.EvictImageData.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.EvictImageData} The cloned message.
+ * @override
+ */
+sketchology.proto.EvictImageData.prototype.clone;
+
+
+/**
+ * Gets the value of the uri field.
+ * @return {?string} The value.
+ */
+sketchology.proto.EvictImageData.prototype.getUri = function() {
+  return /** @type {?string} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the uri field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.EvictImageData.prototype.getUriOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the uri field.
+ * @param {string} value The value.
+ */
+sketchology.proto.EvictImageData.prototype.setUri = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uri field has a value.
+ */
+sketchology.proto.EvictImageData.prototype.hasUri = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uri field.
+ */
+sketchology.proto.EvictImageData.prototype.uriCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uri field.
+ */
+sketchology.proto.EvictImageData.prototype.clearUri = function() {
+  this.clear$Field(1);
+};
+
+
+
+/**
+ * Message Viewport.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.Viewport = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.Viewport, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.Viewport.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.Viewport} The cloned message.
+ * @override
+ */
+sketchology.proto.Viewport.prototype.clone;
+
+
+/**
+ * Gets the value of the fbo_handle field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Viewport.prototype.getFboHandle = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the fbo_handle field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Viewport.prototype.getFboHandleOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the fbo_handle field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Viewport.prototype.setFboHandle = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the fbo_handle field has a value.
+ */
+sketchology.proto.Viewport.prototype.hasFboHandle = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the fbo_handle field.
+ */
+sketchology.proto.Viewport.prototype.fboHandleCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the fbo_handle field.
+ */
+sketchology.proto.Viewport.prototype.clearFboHandle = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the width field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Viewport.prototype.getWidth = function() {
+  return /** @type {?number} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the width field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Viewport.prototype.getWidthOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the width field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Viewport.prototype.setWidth = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the width field has a value.
+ */
+sketchology.proto.Viewport.prototype.hasWidth = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the width field.
+ */
+sketchology.proto.Viewport.prototype.widthCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the width field.
+ */
+sketchology.proto.Viewport.prototype.clearWidth = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the height field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Viewport.prototype.getHeight = function() {
+  return /** @type {?number} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the height field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Viewport.prototype.getHeightOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the height field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Viewport.prototype.setHeight = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the height field has a value.
+ */
+sketchology.proto.Viewport.prototype.hasHeight = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the height field.
+ */
+sketchology.proto.Viewport.prototype.heightCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the height field.
+ */
+sketchology.proto.Viewport.prototype.clearHeight = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the ppi field.
+ * @return {?number} The value.
+ */
+sketchology.proto.Viewport.prototype.getPpi = function() {
+  return /** @type {?number} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the ppi field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.Viewport.prototype.getPpiOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the ppi field.
+ * @param {number} value The value.
+ */
+sketchology.proto.Viewport.prototype.setPpi = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the ppi field has a value.
+ */
+sketchology.proto.Viewport.prototype.hasPpi = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the ppi field.
+ */
+sketchology.proto.Viewport.prototype.ppiCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the ppi field.
+ */
+sketchology.proto.Viewport.prototype.clearPpi = function() {
+  this.clear$Field(4);
+};
+
+
+
+/**
+ * Message ImageExport.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ImageExport = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ImageExport, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ImageExport.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ImageExport} The cloned message.
+ * @override
+ */
+sketchology.proto.ImageExport.prototype.clone;
+
+
+/**
+ * Gets the value of the max_dimension_px field.
+ * @return {?number} The value.
+ */
+sketchology.proto.ImageExport.prototype.getMaxDimensionPx = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the max_dimension_px field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.ImageExport.prototype.getMaxDimensionPxOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the max_dimension_px field.
+ * @param {number} value The value.
+ */
+sketchology.proto.ImageExport.prototype.setMaxDimensionPx = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the max_dimension_px field has a value.
+ */
+sketchology.proto.ImageExport.prototype.hasMaxDimensionPx = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the max_dimension_px field.
+ */
+sketchology.proto.ImageExport.prototype.maxDimensionPxCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the max_dimension_px field.
+ */
+sketchology.proto.ImageExport.prototype.clearMaxDimensionPx = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the should_draw_background field.
+ * @return {?boolean} The value.
+ */
+sketchology.proto.ImageExport.prototype.getShouldDrawBackground = function() {
+  return /** @type {?boolean} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the should_draw_background field or the default value if not set.
+ * @return {boolean} The value.
+ */
+sketchology.proto.ImageExport.prototype.getShouldDrawBackgroundOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the should_draw_background field.
+ * @param {boolean} value The value.
+ */
+sketchology.proto.ImageExport.prototype.setShouldDrawBackground = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the should_draw_background field has a value.
+ */
+sketchology.proto.ImageExport.prototype.hasShouldDrawBackground = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the should_draw_background field.
+ */
+sketchology.proto.ImageExport.prototype.shouldDrawBackgroundCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the should_draw_background field.
+ */
+sketchology.proto.ImageExport.prototype.clearShouldDrawBackground = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message LinearPathAnimation.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.LinearPathAnimation = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.LinearPathAnimation, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.LinearPathAnimation.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.LinearPathAnimation} The cloned message.
+ * @override
+ */
+sketchology.proto.LinearPathAnimation.prototype.clone;
+
+
+/**
+ * Gets the value of the rgba_from field.
+ * @return {?number} The value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.getRgbaFrom = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the rgba_from field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.getRgbaFromOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the rgba_from field.
+ * @param {number} value The value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.setRgbaFrom = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the rgba_from field has a value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.hasRgbaFrom = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the rgba_from field.
+ */
+sketchology.proto.LinearPathAnimation.prototype.rgbaFromCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the rgba_from field.
+ */
+sketchology.proto.LinearPathAnimation.prototype.clearRgbaFrom = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the rgba_seconds field.
+ * @return {?number} The value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.getRgbaSeconds = function() {
+  return /** @type {?number} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the rgba_seconds field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.getRgbaSecondsOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the rgba_seconds field.
+ * @param {number} value The value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.setRgbaSeconds = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the rgba_seconds field has a value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.hasRgbaSeconds = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the rgba_seconds field.
+ */
+sketchology.proto.LinearPathAnimation.prototype.rgbaSecondsCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the rgba_seconds field.
+ */
+sketchology.proto.LinearPathAnimation.prototype.clearRgbaSeconds = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the dilation_from field.
+ * @return {?number} The value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.getDilationFrom = function() {
+  return /** @type {?number} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the dilation_from field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.getDilationFromOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the dilation_from field.
+ * @param {number} value The value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.setDilationFrom = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the dilation_from field has a value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.hasDilationFrom = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the dilation_from field.
+ */
+sketchology.proto.LinearPathAnimation.prototype.dilationFromCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the dilation_from field.
+ */
+sketchology.proto.LinearPathAnimation.prototype.clearDilationFrom = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the dilation_seconds field.
+ * @return {?number} The value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.getDilationSeconds = function() {
+  return /** @type {?number} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the dilation_seconds field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.getDilationSecondsOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the dilation_seconds field.
+ * @param {number} value The value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.setDilationSeconds = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the dilation_seconds field has a value.
+ */
+sketchology.proto.LinearPathAnimation.prototype.hasDilationSeconds = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the dilation_seconds field.
+ */
+sketchology.proto.LinearPathAnimation.prototype.dilationSecondsCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the dilation_seconds field.
+ */
+sketchology.proto.LinearPathAnimation.prototype.clearDilationSeconds = function() {
+  this.clear$Field(4);
+};
+
+
+
+/**
+ * Message LineSize.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.LineSize = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.LineSize, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.LineSize.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.LineSize} The cloned message.
+ * @override
+ */
+sketchology.proto.LineSize.prototype.clone;
+
+
+/**
+ * Gets the value of the stroke_width field.
+ * @return {?number} The value.
+ */
+sketchology.proto.LineSize.prototype.getStrokeWidth = function() {
+  return /** @type {?number} */ (this.get$Value(7));
+};
+
+
+/**
+ * Gets the value of the stroke_width field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.LineSize.prototype.getStrokeWidthOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(7));
+};
+
+
+/**
+ * Sets the value of the stroke_width field.
+ * @param {number} value The value.
+ */
+sketchology.proto.LineSize.prototype.setStrokeWidth = function(value) {
+  this.set$Value(7, value);
+};
+
+
+/**
+ * @return {boolean} Whether the stroke_width field has a value.
+ */
+sketchology.proto.LineSize.prototype.hasStrokeWidth = function() {
+  return this.has$Value(7);
+};
+
+
+/**
+ * @return {number} The number of values in the stroke_width field.
+ */
+sketchology.proto.LineSize.prototype.strokeWidthCount = function() {
+  return this.count$Values(7);
+};
+
+
+/**
+ * Clears the values in the stroke_width field.
+ */
+sketchology.proto.LineSize.prototype.clearStrokeWidth = function() {
+  this.clear$Field(7);
+};
+
+
+/**
+ * Gets the value of the units field.
+ * @return {?sketchology.proto.LineSize.SizeType} The value.
+ */
+sketchology.proto.LineSize.prototype.getUnits = function() {
+  return /** @type {?sketchology.proto.LineSize.SizeType} */ (this.get$Value(8));
+};
+
+
+/**
+ * Gets the value of the units field or the default value if not set.
+ * @return {!sketchology.proto.LineSize.SizeType} The value.
+ */
+sketchology.proto.LineSize.prototype.getUnitsOrDefault = function() {
+  return /** @type {!sketchology.proto.LineSize.SizeType} */ (this.get$ValueOrDefault(8));
+};
+
+
+/**
+ * Sets the value of the units field.
+ * @param {!sketchology.proto.LineSize.SizeType} value The value.
+ */
+sketchology.proto.LineSize.prototype.setUnits = function(value) {
+  this.set$Value(8, value);
+};
+
+
+/**
+ * @return {boolean} Whether the units field has a value.
+ */
+sketchology.proto.LineSize.prototype.hasUnits = function() {
+  return this.has$Value(8);
+};
+
+
+/**
+ * @return {number} The number of values in the units field.
+ */
+sketchology.proto.LineSize.prototype.unitsCount = function() {
+  return this.count$Values(8);
+};
+
+
+/**
+ * Clears the values in the units field.
+ */
+sketchology.proto.LineSize.prototype.clearUnits = function() {
+  this.clear$Field(8);
+};
+
+
+/**
+ * Enumeration SizeType.
+ * @enum {number}
+ */
+sketchology.proto.LineSize.SizeType = {
+  UNKNOWN_SIZE: 0,
+  WORLD_UNITS: 1,
+  POINTS: 2,
+  ZOOM_INDEPENDENT_DP: 3,
+  PERCENT_WORLD: 4,
+  PERCENT_ZOOM_INDEPENDENT: 5
+};
+
+
+
+/**
+ * Message PusherToolParams.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.PusherToolParams = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.PusherToolParams, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.PusherToolParams.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.PusherToolParams} The cloned message.
+ * @override
+ */
+sketchology.proto.PusherToolParams.prototype.clone;
+
+
+/**
+ * Gets the value of the manipulate_stickers field.
+ * @return {?boolean} The value.
+ */
+sketchology.proto.PusherToolParams.prototype.getManipulateStickers = function() {
+  return /** @type {?boolean} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the manipulate_stickers field or the default value if not set.
+ * @return {boolean} The value.
+ */
+sketchology.proto.PusherToolParams.prototype.getManipulateStickersOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the manipulate_stickers field.
+ * @param {boolean} value The value.
+ */
+sketchology.proto.PusherToolParams.prototype.setManipulateStickers = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the manipulate_stickers field has a value.
+ */
+sketchology.proto.PusherToolParams.prototype.hasManipulateStickers = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the manipulate_stickers field.
+ */
+sketchology.proto.PusherToolParams.prototype.manipulateStickersCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the manipulate_stickers field.
+ */
+sketchology.proto.PusherToolParams.prototype.clearManipulateStickers = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the manipulate_text field.
+ * @return {?boolean} The value.
+ */
+sketchology.proto.PusherToolParams.prototype.getManipulateText = function() {
+  return /** @type {?boolean} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the manipulate_text field or the default value if not set.
+ * @return {boolean} The value.
+ */
+sketchology.proto.PusherToolParams.prototype.getManipulateTextOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the manipulate_text field.
+ * @param {boolean} value The value.
+ */
+sketchology.proto.PusherToolParams.prototype.setManipulateText = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the manipulate_text field has a value.
+ */
+sketchology.proto.PusherToolParams.prototype.hasManipulateText = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the manipulate_text field.
+ */
+sketchology.proto.PusherToolParams.prototype.manipulateTextCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the manipulate_text field.
+ */
+sketchology.proto.PusherToolParams.prototype.clearManipulateText = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message ToolParams.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ToolParams = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ToolParams, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ToolParams.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ToolParams} The cloned message.
+ * @override
+ */
+sketchology.proto.ToolParams.prototype.clone;
+
+
+/**
+ * Gets the value of the tool field.
+ * @return {?sketchology.proto.ToolParams.ToolType} The value.
+ */
+sketchology.proto.ToolParams.prototype.getTool = function() {
+  return /** @type {?sketchology.proto.ToolParams.ToolType} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the tool field or the default value if not set.
+ * @return {!sketchology.proto.ToolParams.ToolType} The value.
+ */
+sketchology.proto.ToolParams.prototype.getToolOrDefault = function() {
+  return /** @type {!sketchology.proto.ToolParams.ToolType} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the tool field.
+ * @param {!sketchology.proto.ToolParams.ToolType} value The value.
+ */
+sketchology.proto.ToolParams.prototype.setTool = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the tool field has a value.
+ */
+sketchology.proto.ToolParams.prototype.hasTool = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the tool field.
+ */
+sketchology.proto.ToolParams.prototype.toolCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the tool field.
+ */
+sketchology.proto.ToolParams.prototype.clearTool = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the rgba field.
+ * @return {?number} The value.
+ */
+sketchology.proto.ToolParams.prototype.getRgba = function() {
+  return /** @type {?number} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the rgba field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.ToolParams.prototype.getRgbaOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the rgba field.
+ * @param {number} value The value.
+ */
+sketchology.proto.ToolParams.prototype.setRgba = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the rgba field has a value.
+ */
+sketchology.proto.ToolParams.prototype.hasRgba = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the rgba field.
+ */
+sketchology.proto.ToolParams.prototype.rgbaCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the rgba field.
+ */
+sketchology.proto.ToolParams.prototype.clearRgba = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the line_size field.
+ * @return {?sketchology.proto.LineSize} The value.
+ */
+sketchology.proto.ToolParams.prototype.getLineSize = function() {
+  return /** @type {?sketchology.proto.LineSize} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the line_size field or the default value if not set.
+ * @return {!sketchology.proto.LineSize} The value.
+ */
+sketchology.proto.ToolParams.prototype.getLineSizeOrDefault = function() {
+  return /** @type {!sketchology.proto.LineSize} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the line_size field.
+ * @param {!sketchology.proto.LineSize} value The value.
+ */
+sketchology.proto.ToolParams.prototype.setLineSize = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the line_size field has a value.
+ */
+sketchology.proto.ToolParams.prototype.hasLineSize = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the line_size field.
+ */
+sketchology.proto.ToolParams.prototype.lineSizeCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the line_size field.
+ */
+sketchology.proto.ToolParams.prototype.clearLineSize = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the pusher_tool_params field.
+ * @return {?sketchology.proto.PusherToolParams} The value.
+ */
+sketchology.proto.ToolParams.prototype.getPusherToolParams = function() {
+  return /** @type {?sketchology.proto.PusherToolParams} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the pusher_tool_params field or the default value if not set.
+ * @return {!sketchology.proto.PusherToolParams} The value.
+ */
+sketchology.proto.ToolParams.prototype.getPusherToolParamsOrDefault = function() {
+  return /** @type {!sketchology.proto.PusherToolParams} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the pusher_tool_params field.
+ * @param {!sketchology.proto.PusherToolParams} value The value.
+ */
+sketchology.proto.ToolParams.prototype.setPusherToolParams = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the pusher_tool_params field has a value.
+ */
+sketchology.proto.ToolParams.prototype.hasPusherToolParams = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the pusher_tool_params field.
+ */
+sketchology.proto.ToolParams.prototype.pusherToolParamsCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the pusher_tool_params field.
+ */
+sketchology.proto.ToolParams.prototype.clearPusherToolParams = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Gets the value of the brush_type field.
+ * @return {?sketchology.proto.BrushType} The value.
+ */
+sketchology.proto.ToolParams.prototype.getBrushType = function() {
+  return /** @type {?sketchology.proto.BrushType} */ (this.get$Value(5));
+};
+
+
+/**
+ * Gets the value of the brush_type field or the default value if not set.
+ * @return {!sketchology.proto.BrushType} The value.
+ */
+sketchology.proto.ToolParams.prototype.getBrushTypeOrDefault = function() {
+  return /** @type {!sketchology.proto.BrushType} */ (this.get$ValueOrDefault(5));
+};
+
+
+/**
+ * Sets the value of the brush_type field.
+ * @param {!sketchology.proto.BrushType} value The value.
+ */
+sketchology.proto.ToolParams.prototype.setBrushType = function(value) {
+  this.set$Value(5, value);
+};
+
+
+/**
+ * @return {boolean} Whether the brush_type field has a value.
+ */
+sketchology.proto.ToolParams.prototype.hasBrushType = function() {
+  return this.has$Value(5);
+};
+
+
+/**
+ * @return {number} The number of values in the brush_type field.
+ */
+sketchology.proto.ToolParams.prototype.brushTypeCount = function() {
+  return this.count$Values(5);
+};
+
+
+/**
+ * Clears the values in the brush_type field.
+ */
+sketchology.proto.ToolParams.prototype.clearBrushType = function() {
+  this.clear$Field(5);
+};
+
+
+/**
+ * Gets the value of the linear_path_animation field.
+ * @return {?sketchology.proto.LinearPathAnimation} The value.
+ */
+sketchology.proto.ToolParams.prototype.getLinearPathAnimation = function() {
+  return /** @type {?sketchology.proto.LinearPathAnimation} */ (this.get$Value(6));
+};
+
+
+/**
+ * Gets the value of the linear_path_animation field or the default value if not set.
+ * @return {!sketchology.proto.LinearPathAnimation} The value.
+ */
+sketchology.proto.ToolParams.prototype.getLinearPathAnimationOrDefault = function() {
+  return /** @type {!sketchology.proto.LinearPathAnimation} */ (this.get$ValueOrDefault(6));
+};
+
+
+/**
+ * Sets the value of the linear_path_animation field.
+ * @param {!sketchology.proto.LinearPathAnimation} value The value.
+ */
+sketchology.proto.ToolParams.prototype.setLinearPathAnimation = function(value) {
+  this.set$Value(6, value);
+};
+
+
+/**
+ * @return {boolean} Whether the linear_path_animation field has a value.
+ */
+sketchology.proto.ToolParams.prototype.hasLinearPathAnimation = function() {
+  return this.has$Value(6);
+};
+
+
+/**
+ * @return {number} The number of values in the linear_path_animation field.
+ */
+sketchology.proto.ToolParams.prototype.linearPathAnimationCount = function() {
+  return this.count$Values(6);
+};
+
+
+/**
+ * Clears the values in the linear_path_animation field.
+ */
+sketchology.proto.ToolParams.prototype.clearLinearPathAnimation = function() {
+  this.clear$Field(6);
+};
+
+
+/**
+ * Enumeration ToolType.
+ * @enum {number}
+ */
+sketchology.proto.ToolParams.ToolType = {
+  UNKNOWN: 0,
+  LINE: 1,
+  EDIT: 2,
+  MAGIC_ERASE: 3,
+  QUERY: 4,
+  NOTOOL: 5,
+  FILTER_CHOOSER: 6,
+  PUSHER: 7,
+  CROP: 8
+};
+
+
+
+/**
+ * Message FlagAssignment.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.FlagAssignment = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.FlagAssignment, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.FlagAssignment.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.FlagAssignment} The cloned message.
+ * @override
+ */
+sketchology.proto.FlagAssignment.prototype.clone;
+
+
+/**
+ * Gets the value of the flag field.
+ * @return {?sketchology.proto.Flag} The value.
+ */
+sketchology.proto.FlagAssignment.prototype.getFlag = function() {
+  return /** @type {?sketchology.proto.Flag} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the flag field or the default value if not set.
+ * @return {!sketchology.proto.Flag} The value.
+ */
+sketchology.proto.FlagAssignment.prototype.getFlagOrDefault = function() {
+  return /** @type {!sketchology.proto.Flag} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the flag field.
+ * @param {!sketchology.proto.Flag} value The value.
+ */
+sketchology.proto.FlagAssignment.prototype.setFlag = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the flag field has a value.
+ */
+sketchology.proto.FlagAssignment.prototype.hasFlag = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the flag field.
+ */
+sketchology.proto.FlagAssignment.prototype.flagCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the flag field.
+ */
+sketchology.proto.FlagAssignment.prototype.clearFlag = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the bool_value field.
+ * @return {?boolean} The value.
+ */
+sketchology.proto.FlagAssignment.prototype.getBoolValue = function() {
+  return /** @type {?boolean} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the bool_value field or the default value if not set.
+ * @return {boolean} The value.
+ */
+sketchology.proto.FlagAssignment.prototype.getBoolValueOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the bool_value field.
+ * @param {boolean} value The value.
+ */
+sketchology.proto.FlagAssignment.prototype.setBoolValue = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the bool_value field has a value.
+ */
+sketchology.proto.FlagAssignment.prototype.hasBoolValue = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the bool_value field.
+ */
+sketchology.proto.FlagAssignment.prototype.boolValueCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the bool_value field.
+ */
+sketchology.proto.FlagAssignment.prototype.clearBoolValue = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message AddElement.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.AddElement = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.AddElement, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.AddElement.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.AddElement} The cloned message.
+ * @override
+ */
+sketchology.proto.AddElement.prototype.clone;
+
+
+/**
+ * Gets the value of the bundle field.
+ * @return {?sketchology.proto.ElementBundle} The value.
+ */
+sketchology.proto.AddElement.prototype.getBundle = function() {
+  return /** @type {?sketchology.proto.ElementBundle} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the bundle field or the default value if not set.
+ * @return {!sketchology.proto.ElementBundle} The value.
+ */
+sketchology.proto.AddElement.prototype.getBundleOrDefault = function() {
+  return /** @type {!sketchology.proto.ElementBundle} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the bundle field.
+ * @param {!sketchology.proto.ElementBundle} value The value.
+ */
+sketchology.proto.AddElement.prototype.setBundle = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the bundle field has a value.
+ */
+sketchology.proto.AddElement.prototype.hasBundle = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the bundle field.
+ */
+sketchology.proto.AddElement.prototype.bundleCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the bundle field.
+ */
+sketchology.proto.AddElement.prototype.clearBundle = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the below_element_with_uuid field.
+ * @return {?string} The value.
+ */
+sketchology.proto.AddElement.prototype.getBelowElementWithUuid = function() {
+  return /** @type {?string} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the below_element_with_uuid field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.AddElement.prototype.getBelowElementWithUuidOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the below_element_with_uuid field.
+ * @param {string} value The value.
+ */
+sketchology.proto.AddElement.prototype.setBelowElementWithUuid = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the below_element_with_uuid field has a value.
+ */
+sketchology.proto.AddElement.prototype.hasBelowElementWithUuid = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the below_element_with_uuid field.
+ */
+sketchology.proto.AddElement.prototype.belowElementWithUuidCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the below_element_with_uuid field.
+ */
+sketchology.proto.AddElement.prototype.clearBelowElementWithUuid = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message OutOfBoundsColor.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.OutOfBoundsColor = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.OutOfBoundsColor, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.OutOfBoundsColor.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.OutOfBoundsColor} The cloned message.
+ * @override
+ */
+sketchology.proto.OutOfBoundsColor.prototype.clone;
+
+
+/**
+ * Gets the value of the rgba field.
+ * @return {?number} The value.
+ */
+sketchology.proto.OutOfBoundsColor.prototype.getRgba = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the rgba field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.OutOfBoundsColor.prototype.getRgbaOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the rgba field.
+ * @param {number} value The value.
+ */
+sketchology.proto.OutOfBoundsColor.prototype.setRgba = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the rgba field has a value.
+ */
+sketchology.proto.OutOfBoundsColor.prototype.hasRgba = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the rgba field.
+ */
+sketchology.proto.OutOfBoundsColor.prototype.rgbaCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the rgba field.
+ */
+sketchology.proto.OutOfBoundsColor.prototype.clearRgba = function() {
+  this.clear$Field(1);
+};
+
+
+
+/**
+ * Message SInputStream.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.SInputStream = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.SInputStream, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.SInputStream.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.SInputStream} The cloned message.
+ * @override
+ */
+sketchology.proto.SInputStream.prototype.clone;
+
+
+/**
+ * Gets the value of the screen_width field.
+ * @return {?number} The value.
+ */
+sketchology.proto.SInputStream.prototype.getScreenWidth = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the screen_width field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.SInputStream.prototype.getScreenWidthOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the screen_width field.
+ * @param {number} value The value.
+ */
+sketchology.proto.SInputStream.prototype.setScreenWidth = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the screen_width field has a value.
+ */
+sketchology.proto.SInputStream.prototype.hasScreenWidth = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the screen_width field.
+ */
+sketchology.proto.SInputStream.prototype.screenWidthCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the screen_width field.
+ */
+sketchology.proto.SInputStream.prototype.clearScreenWidth = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the screen_height field.
+ * @return {?number} The value.
+ */
+sketchology.proto.SInputStream.prototype.getScreenHeight = function() {
+  return /** @type {?number} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the screen_height field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.SInputStream.prototype.getScreenHeightOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the screen_height field.
+ * @param {number} value The value.
+ */
+sketchology.proto.SInputStream.prototype.setScreenHeight = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the screen_height field has a value.
+ */
+sketchology.proto.SInputStream.prototype.hasScreenHeight = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the screen_height field.
+ */
+sketchology.proto.SInputStream.prototype.screenHeightCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the screen_height field.
+ */
+sketchology.proto.SInputStream.prototype.clearScreenHeight = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the screen_ppi field.
+ * @return {?number} The value.
+ */
+sketchology.proto.SInputStream.prototype.getScreenPpi = function() {
+  return /** @type {?number} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the screen_ppi field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.SInputStream.prototype.getScreenPpiOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the screen_ppi field.
+ * @param {number} value The value.
+ */
+sketchology.proto.SInputStream.prototype.setScreenPpi = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the screen_ppi field has a value.
+ */
+sketchology.proto.SInputStream.prototype.hasScreenPpi = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the screen_ppi field.
+ */
+sketchology.proto.SInputStream.prototype.screenPpiCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the screen_ppi field.
+ */
+sketchology.proto.SInputStream.prototype.clearScreenPpi = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the input field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.SInput} The value.
+ */
+sketchology.proto.SInputStream.prototype.getInput = function(index) {
+  return /** @type {?sketchology.proto.SInput} */ (this.get$Value(4, index));
+};
+
+
+/**
+ * Gets the value of the input field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.SInput} The value.
+ */
+sketchology.proto.SInputStream.prototype.getInputOrDefault = function(index) {
+  return /** @type {!sketchology.proto.SInput} */ (this.get$ValueOrDefault(4, index));
+};
+
+
+/**
+ * Adds a value to the input field.
+ * @param {!sketchology.proto.SInput} value The value to add.
+ */
+sketchology.proto.SInputStream.prototype.addInput = function(value) {
+  this.add$Value(4, value);
+};
+
+
+/**
+ * Returns the array of values in the input field.
+ * @return {!Array<!sketchology.proto.SInput>} The values in the field.
+ */
+sketchology.proto.SInputStream.prototype.inputArray = function() {
+  return /** @type {!Array<!sketchology.proto.SInput>} */ (this.array$Values(4));
+};
+
+
+/**
+ * @return {boolean} Whether the input field has a value.
+ */
+sketchology.proto.SInputStream.prototype.hasInput = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the input field.
+ */
+sketchology.proto.SInputStream.prototype.inputCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the input field.
+ */
+sketchology.proto.SInputStream.prototype.clearInput = function() {
+  this.clear$Field(4);
+};
+
+
+
+/**
+ * Message SInput.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.SInput = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.SInput, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.SInput.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.SInput} The cloned message.
+ * @override
+ */
+sketchology.proto.SInput.prototype.clone;
+
+
+/**
+ * Gets the value of the type field.
+ * @return {?sketchology.proto.SInput.InputType} The value.
+ */
+sketchology.proto.SInput.prototype.getType = function() {
+  return /** @type {?sketchology.proto.SInput.InputType} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the type field or the default value if not set.
+ * @return {!sketchology.proto.SInput.InputType} The value.
+ */
+sketchology.proto.SInput.prototype.getTypeOrDefault = function() {
+  return /** @type {!sketchology.proto.SInput.InputType} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the type field.
+ * @param {!sketchology.proto.SInput.InputType} value The value.
+ */
+sketchology.proto.SInput.prototype.setType = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the type field has a value.
+ */
+sketchology.proto.SInput.prototype.hasType = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the type field.
+ */
+sketchology.proto.SInput.prototype.typeCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the type field.
+ */
+sketchology.proto.SInput.prototype.clearType = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the id field.
+ * @return {?number} The value.
+ */
+sketchology.proto.SInput.prototype.getId = function() {
+  return /** @type {?number} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the id field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.SInput.prototype.getIdOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the id field.
+ * @param {number} value The value.
+ */
+sketchology.proto.SInput.prototype.setId = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the id field has a value.
+ */
+sketchology.proto.SInput.prototype.hasId = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the id field.
+ */
+sketchology.proto.SInput.prototype.idCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the id field.
+ */
+sketchology.proto.SInput.prototype.clearId = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the flags field.
+ * @return {?number} The value.
+ */
+sketchology.proto.SInput.prototype.getFlags = function() {
+  return /** @type {?number} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the flags field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.SInput.prototype.getFlagsOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the flags field.
+ * @param {number} value The value.
+ */
+sketchology.proto.SInput.prototype.setFlags = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the flags field has a value.
+ */
+sketchology.proto.SInput.prototype.hasFlags = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the flags field.
+ */
+sketchology.proto.SInput.prototype.flagsCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the flags field.
+ */
+sketchology.proto.SInput.prototype.clearFlags = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the time_s field.
+ * @return {?number} The value.
+ */
+sketchology.proto.SInput.prototype.getTimeS = function() {
+  return /** @type {?number} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the time_s field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.SInput.prototype.getTimeSOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the time_s field.
+ * @param {number} value The value.
+ */
+sketchology.proto.SInput.prototype.setTimeS = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the time_s field has a value.
+ */
+sketchology.proto.SInput.prototype.hasTimeS = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the time_s field.
+ */
+sketchology.proto.SInput.prototype.timeSCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the time_s field.
+ */
+sketchology.proto.SInput.prototype.clearTimeS = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Gets the value of the screen_pos_x field.
+ * @return {?number} The value.
+ */
+sketchology.proto.SInput.prototype.getScreenPosX = function() {
+  return /** @type {?number} */ (this.get$Value(5));
+};
+
+
+/**
+ * Gets the value of the screen_pos_x field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.SInput.prototype.getScreenPosXOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(5));
+};
+
+
+/**
+ * Sets the value of the screen_pos_x field.
+ * @param {number} value The value.
+ */
+sketchology.proto.SInput.prototype.setScreenPosX = function(value) {
+  this.set$Value(5, value);
+};
+
+
+/**
+ * @return {boolean} Whether the screen_pos_x field has a value.
+ */
+sketchology.proto.SInput.prototype.hasScreenPosX = function() {
+  return this.has$Value(5);
+};
+
+
+/**
+ * @return {number} The number of values in the screen_pos_x field.
+ */
+sketchology.proto.SInput.prototype.screenPosXCount = function() {
+  return this.count$Values(5);
+};
+
+
+/**
+ * Clears the values in the screen_pos_x field.
+ */
+sketchology.proto.SInput.prototype.clearScreenPosX = function() {
+  this.clear$Field(5);
+};
+
+
+/**
+ * Gets the value of the screen_pos_y field.
+ * @return {?number} The value.
+ */
+sketchology.proto.SInput.prototype.getScreenPosY = function() {
+  return /** @type {?number} */ (this.get$Value(6));
+};
+
+
+/**
+ * Gets the value of the screen_pos_y field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.SInput.prototype.getScreenPosYOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(6));
+};
+
+
+/**
+ * Sets the value of the screen_pos_y field.
+ * @param {number} value The value.
+ */
+sketchology.proto.SInput.prototype.setScreenPosY = function(value) {
+  this.set$Value(6, value);
+};
+
+
+/**
+ * @return {boolean} Whether the screen_pos_y field has a value.
+ */
+sketchology.proto.SInput.prototype.hasScreenPosY = function() {
+  return this.has$Value(6);
+};
+
+
+/**
+ * @return {number} The number of values in the screen_pos_y field.
+ */
+sketchology.proto.SInput.prototype.screenPosYCount = function() {
+  return this.count$Values(6);
+};
+
+
+/**
+ * Clears the values in the screen_pos_y field.
+ */
+sketchology.proto.SInput.prototype.clearScreenPosY = function() {
+  this.clear$Field(6);
+};
+
+
+/**
+ * Gets the value of the pressure field.
+ * @return {?number} The value.
+ */
+sketchology.proto.SInput.prototype.getPressure = function() {
+  return /** @type {?number} */ (this.get$Value(7));
+};
+
+
+/**
+ * Gets the value of the pressure field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.SInput.prototype.getPressureOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(7));
+};
+
+
+/**
+ * Sets the value of the pressure field.
+ * @param {number} value The value.
+ */
+sketchology.proto.SInput.prototype.setPressure = function(value) {
+  this.set$Value(7, value);
+};
+
+
+/**
+ * @return {boolean} Whether the pressure field has a value.
+ */
+sketchology.proto.SInput.prototype.hasPressure = function() {
+  return this.has$Value(7);
+};
+
+
+/**
+ * @return {number} The number of values in the pressure field.
+ */
+sketchology.proto.SInput.prototype.pressureCount = function() {
+  return this.count$Values(7);
+};
+
+
+/**
+ * Clears the values in the pressure field.
+ */
+sketchology.proto.SInput.prototype.clearPressure = function() {
+  this.clear$Field(7);
+};
+
+
+/**
+ * Gets the value of the wheel_delta field.
+ * @return {?number} The value.
+ */
+sketchology.proto.SInput.prototype.getWheelDelta = function() {
+  return /** @type {?number} */ (this.get$Value(8));
+};
+
+
+/**
+ * Gets the value of the wheel_delta field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.SInput.prototype.getWheelDeltaOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(8));
+};
+
+
+/**
+ * Sets the value of the wheel_delta field.
+ * @param {number} value The value.
+ */
+sketchology.proto.SInput.prototype.setWheelDelta = function(value) {
+  this.set$Value(8, value);
+};
+
+
+/**
+ * @return {boolean} Whether the wheel_delta field has a value.
+ */
+sketchology.proto.SInput.prototype.hasWheelDelta = function() {
+  return this.has$Value(8);
+};
+
+
+/**
+ * @return {number} The number of values in the wheel_delta field.
+ */
+sketchology.proto.SInput.prototype.wheelDeltaCount = function() {
+  return this.count$Values(8);
+};
+
+
+/**
+ * Clears the values in the wheel_delta field.
+ */
+sketchology.proto.SInput.prototype.clearWheelDelta = function() {
+  this.clear$Field(8);
+};
+
+
+/**
+ * Gets the value of the tilt field.
+ * @return {?number} The value.
+ */
+sketchology.proto.SInput.prototype.getTilt = function() {
+  return /** @type {?number} */ (this.get$Value(9));
+};
+
+
+/**
+ * Gets the value of the tilt field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.SInput.prototype.getTiltOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(9));
+};
+
+
+/**
+ * Sets the value of the tilt field.
+ * @param {number} value The value.
+ */
+sketchology.proto.SInput.prototype.setTilt = function(value) {
+  this.set$Value(9, value);
+};
+
+
+/**
+ * @return {boolean} Whether the tilt field has a value.
+ */
+sketchology.proto.SInput.prototype.hasTilt = function() {
+  return this.has$Value(9);
+};
+
+
+/**
+ * @return {number} The number of values in the tilt field.
+ */
+sketchology.proto.SInput.prototype.tiltCount = function() {
+  return this.count$Values(9);
+};
+
+
+/**
+ * Clears the values in the tilt field.
+ */
+sketchology.proto.SInput.prototype.clearTilt = function() {
+  this.clear$Field(9);
+};
+
+
+/**
+ * Gets the value of the orientation field.
+ * @return {?number} The value.
+ */
+sketchology.proto.SInput.prototype.getOrientation = function() {
+  return /** @type {?number} */ (this.get$Value(10));
+};
+
+
+/**
+ * Gets the value of the orientation field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.SInput.prototype.getOrientationOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(10));
+};
+
+
+/**
+ * Sets the value of the orientation field.
+ * @param {number} value The value.
+ */
+sketchology.proto.SInput.prototype.setOrientation = function(value) {
+  this.set$Value(10, value);
+};
+
+
+/**
+ * @return {boolean} Whether the orientation field has a value.
+ */
+sketchology.proto.SInput.prototype.hasOrientation = function() {
+  return this.has$Value(10);
+};
+
+
+/**
+ * @return {number} The number of values in the orientation field.
+ */
+sketchology.proto.SInput.prototype.orientationCount = function() {
+  return this.count$Values(10);
+};
+
+
+/**
+ * Clears the values in the orientation field.
+ */
+sketchology.proto.SInput.prototype.clearOrientation = function() {
+  this.clear$Field(10);
+};
+
+
+/**
+ * Enumeration InputType.
+ * @enum {number}
+ */
+sketchology.proto.SInput.InputType = {
+  UNKNOWN: 0,
+  MOUSE: 1,
+  TOUCH: 2,
+  PEN: 3,
+  ERASER: 4
+};
+
+
+
+/**
+ * Message SimulatedInput.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.SimulatedInput = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.SimulatedInput, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.SimulatedInput.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.SimulatedInput} The cloned message.
+ * @override
+ */
+sketchology.proto.SimulatedInput.prototype.clone;
+
+
+/**
+ * Gets the value of the xs field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?number} The value.
+ */
+sketchology.proto.SimulatedInput.prototype.getXs = function(index) {
+  return /** @type {?number} */ (this.get$Value(1, index));
+};
+
+
+/**
+ * Gets the value of the xs field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {number} The value.
+ */
+sketchology.proto.SimulatedInput.prototype.getXsOrDefault = function(index) {
+  return /** @type {number} */ (this.get$ValueOrDefault(1, index));
+};
+
+
+/**
+ * Adds a value to the xs field.
+ * @param {number} value The value to add.
+ */
+sketchology.proto.SimulatedInput.prototype.addXs = function(value) {
+  this.add$Value(1, value);
+};
+
+
+/**
+ * Returns the array of values in the xs field.
+ * @return {!Array<number>} The values in the field.
+ */
+sketchology.proto.SimulatedInput.prototype.xsArray = function() {
+  return /** @type {!Array<number>} */ (this.array$Values(1));
+};
+
+
+/**
+ * @return {boolean} Whether the xs field has a value.
+ */
+sketchology.proto.SimulatedInput.prototype.hasXs = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the xs field.
+ */
+sketchology.proto.SimulatedInput.prototype.xsCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the xs field.
+ */
+sketchology.proto.SimulatedInput.prototype.clearXs = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the ys field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?number} The value.
+ */
+sketchology.proto.SimulatedInput.prototype.getYs = function(index) {
+  return /** @type {?number} */ (this.get$Value(2, index));
+};
+
+
+/**
+ * Gets the value of the ys field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {number} The value.
+ */
+sketchology.proto.SimulatedInput.prototype.getYsOrDefault = function(index) {
+  return /** @type {number} */ (this.get$ValueOrDefault(2, index));
+};
+
+
+/**
+ * Adds a value to the ys field.
+ * @param {number} value The value to add.
+ */
+sketchology.proto.SimulatedInput.prototype.addYs = function(value) {
+  this.add$Value(2, value);
+};
+
+
+/**
+ * Returns the array of values in the ys field.
+ * @return {!Array<number>} The values in the field.
+ */
+sketchology.proto.SimulatedInput.prototype.ysArray = function() {
+  return /** @type {!Array<number>} */ (this.array$Values(2));
+};
+
+
+/**
+ * @return {boolean} Whether the ys field has a value.
+ */
+sketchology.proto.SimulatedInput.prototype.hasYs = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the ys field.
+ */
+sketchology.proto.SimulatedInput.prototype.ysCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the ys field.
+ */
+sketchology.proto.SimulatedInput.prototype.clearYs = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the ts_secs field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?number} The value.
+ */
+sketchology.proto.SimulatedInput.prototype.getTsSecs = function(index) {
+  return /** @type {?number} */ (this.get$Value(3, index));
+};
+
+
+/**
+ * Gets the value of the ts_secs field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {number} The value.
+ */
+sketchology.proto.SimulatedInput.prototype.getTsSecsOrDefault = function(index) {
+  return /** @type {number} */ (this.get$ValueOrDefault(3, index));
+};
+
+
+/**
+ * Adds a value to the ts_secs field.
+ * @param {number} value The value to add.
+ */
+sketchology.proto.SimulatedInput.prototype.addTsSecs = function(value) {
+  this.add$Value(3, value);
+};
+
+
+/**
+ * Returns the array of values in the ts_secs field.
+ * @return {!Array<number>} The values in the field.
+ */
+sketchology.proto.SimulatedInput.prototype.tsSecsArray = function() {
+  return /** @type {!Array<number>} */ (this.array$Values(3));
+};
+
+
+/**
+ * @return {boolean} Whether the ts_secs field has a value.
+ */
+sketchology.proto.SimulatedInput.prototype.hasTsSecs = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the ts_secs field.
+ */
+sketchology.proto.SimulatedInput.prototype.tsSecsCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the ts_secs field.
+ */
+sketchology.proto.SimulatedInput.prototype.clearTsSecs = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the source_details field.
+ * @return {?sketchology.proto.SourceDetails} The value.
+ */
+sketchology.proto.SimulatedInput.prototype.getSourceDetails = function() {
+  return /** @type {?sketchology.proto.SourceDetails} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the source_details field or the default value if not set.
+ * @return {!sketchology.proto.SourceDetails} The value.
+ */
+sketchology.proto.SimulatedInput.prototype.getSourceDetailsOrDefault = function() {
+  return /** @type {!sketchology.proto.SourceDetails} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the source_details field.
+ * @param {!sketchology.proto.SourceDetails} value The value.
+ */
+sketchology.proto.SimulatedInput.prototype.setSourceDetails = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the source_details field has a value.
+ */
+sketchology.proto.SimulatedInput.prototype.hasSourceDetails = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the source_details field.
+ */
+sketchology.proto.SimulatedInput.prototype.sourceDetailsCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the source_details field.
+ */
+sketchology.proto.SimulatedInput.prototype.clearSourceDetails = function() {
+  this.clear$Field(4);
+};
+
+
+
+/**
+ * Message SequencePoint.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.SequencePoint = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.SequencePoint, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.SequencePoint.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.SequencePoint} The cloned message.
+ * @override
+ */
+sketchology.proto.SequencePoint.prototype.clone;
+
+
+/**
+ * Gets the value of the id field.
+ * @return {?number} The value.
+ */
+sketchology.proto.SequencePoint.prototype.getId = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the id field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.SequencePoint.prototype.getIdOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the id field.
+ * @param {number} value The value.
+ */
+sketchology.proto.SequencePoint.prototype.setId = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the id field has a value.
+ */
+sketchology.proto.SequencePoint.prototype.hasId = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the id field.
+ */
+sketchology.proto.SequencePoint.prototype.idCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the id field.
+ */
+sketchology.proto.SequencePoint.prototype.clearId = function() {
+  this.clear$Field(1);
+};
+
+
+
+/**
+ * Message SetCallbackFlags.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.SetCallbackFlags = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.SetCallbackFlags, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.SetCallbackFlags.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.SetCallbackFlags} The cloned message.
+ * @override
+ */
+sketchology.proto.SetCallbackFlags.prototype.clone;
+
+
+/**
+ * Gets the value of the source_details field.
+ * @return {?sketchology.proto.SourceDetails} The value.
+ */
+sketchology.proto.SetCallbackFlags.prototype.getSourceDetails = function() {
+  return /** @type {?sketchology.proto.SourceDetails} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the source_details field or the default value if not set.
+ * @return {!sketchology.proto.SourceDetails} The value.
+ */
+sketchology.proto.SetCallbackFlags.prototype.getSourceDetailsOrDefault = function() {
+  return /** @type {!sketchology.proto.SourceDetails} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the source_details field.
+ * @param {!sketchology.proto.SourceDetails} value The value.
+ */
+sketchology.proto.SetCallbackFlags.prototype.setSourceDetails = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the source_details field has a value.
+ */
+sketchology.proto.SetCallbackFlags.prototype.hasSourceDetails = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the source_details field.
+ */
+sketchology.proto.SetCallbackFlags.prototype.sourceDetailsCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the source_details field.
+ */
+sketchology.proto.SetCallbackFlags.prototype.clearSourceDetails = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the callback_flags field.
+ * @return {?sketchology.proto.CallbackFlags} The value.
+ */
+sketchology.proto.SetCallbackFlags.prototype.getCallbackFlags = function() {
+  return /** @type {?sketchology.proto.CallbackFlags} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the callback_flags field or the default value if not set.
+ * @return {!sketchology.proto.CallbackFlags} The value.
+ */
+sketchology.proto.SetCallbackFlags.prototype.getCallbackFlagsOrDefault = function() {
+  return /** @type {!sketchology.proto.CallbackFlags} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the callback_flags field.
+ * @param {!sketchology.proto.CallbackFlags} value The value.
+ */
+sketchology.proto.SetCallbackFlags.prototype.setCallbackFlags = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the callback_flags field has a value.
+ */
+sketchology.proto.SetCallbackFlags.prototype.hasCallbackFlags = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the callback_flags field.
+ */
+sketchology.proto.SetCallbackFlags.prototype.callbackFlagsCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the callback_flags field.
+ */
+sketchology.proto.SetCallbackFlags.prototype.clearCallbackFlags = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message EngineState.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.EngineState = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.EngineState, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.EngineState.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.EngineState} The cloned message.
+ * @override
+ */
+sketchology.proto.EngineState.prototype.clone;
+
+
+/**
+ * Gets the value of the camera_position field.
+ * @return {?sketchology.proto.Rect} The value.
+ */
+sketchology.proto.EngineState.prototype.getCameraPosition = function() {
+  return /** @type {?sketchology.proto.Rect} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the camera_position field or the default value if not set.
+ * @return {!sketchology.proto.Rect} The value.
+ */
+sketchology.proto.EngineState.prototype.getCameraPositionOrDefault = function() {
+  return /** @type {!sketchology.proto.Rect} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the camera_position field.
+ * @param {!sketchology.proto.Rect} value The value.
+ */
+sketchology.proto.EngineState.prototype.setCameraPosition = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the camera_position field has a value.
+ */
+sketchology.proto.EngineState.prototype.hasCameraPosition = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the camera_position field.
+ */
+sketchology.proto.EngineState.prototype.cameraPositionCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the camera_position field.
+ */
+sketchology.proto.EngineState.prototype.clearCameraPosition = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the page_bounds field.
+ * @return {?sketchology.proto.Rect} The value.
+ */
+sketchology.proto.EngineState.prototype.getPageBounds = function() {
+  return /** @type {?sketchology.proto.Rect} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the page_bounds field or the default value if not set.
+ * @return {!sketchology.proto.Rect} The value.
+ */
+sketchology.proto.EngineState.prototype.getPageBoundsOrDefault = function() {
+  return /** @type {!sketchology.proto.Rect} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the page_bounds field.
+ * @param {!sketchology.proto.Rect} value The value.
+ */
+sketchology.proto.EngineState.prototype.setPageBounds = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the page_bounds field has a value.
+ */
+sketchology.proto.EngineState.prototype.hasPageBounds = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the page_bounds field.
+ */
+sketchology.proto.EngineState.prototype.pageBoundsCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the page_bounds field.
+ */
+sketchology.proto.EngineState.prototype.clearPageBounds = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the selection_is_live field.
+ * @return {?boolean} The value.
+ */
+sketchology.proto.EngineState.prototype.getSelectionIsLive = function() {
+  return /** @type {?boolean} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the selection_is_live field or the default value if not set.
+ * @return {boolean} The value.
+ */
+sketchology.proto.EngineState.prototype.getSelectionIsLiveOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the selection_is_live field.
+ * @param {boolean} value The value.
+ */
+sketchology.proto.EngineState.prototype.setSelectionIsLive = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the selection_is_live field has a value.
+ */
+sketchology.proto.EngineState.prototype.hasSelectionIsLive = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the selection_is_live field.
+ */
+sketchology.proto.EngineState.prototype.selectionIsLiveCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the selection_is_live field.
+ */
+sketchology.proto.EngineState.prototype.clearSelectionIsLive = function() {
+  this.clear$Field(3);
+};
+
+
+
+/**
+ * Message CameraBoundsConfig.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.CameraBoundsConfig = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.CameraBoundsConfig, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.CameraBoundsConfig.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.CameraBoundsConfig} The cloned message.
+ * @override
+ */
+sketchology.proto.CameraBoundsConfig.prototype.clone;
+
+
+/**
+ * Gets the value of the margin_left_px field.
+ * @return {?number} The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.getMarginLeftPx = function() {
+  return /** @type {?number} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the margin_left_px field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.getMarginLeftPxOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the margin_left_px field.
+ * @param {number} value The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.setMarginLeftPx = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the margin_left_px field has a value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.hasMarginLeftPx = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the margin_left_px field.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.marginLeftPxCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the margin_left_px field.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.clearMarginLeftPx = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the margin_right_px field.
+ * @return {?number} The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.getMarginRightPx = function() {
+  return /** @type {?number} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the margin_right_px field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.getMarginRightPxOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the margin_right_px field.
+ * @param {number} value The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.setMarginRightPx = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the margin_right_px field has a value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.hasMarginRightPx = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the margin_right_px field.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.marginRightPxCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the margin_right_px field.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.clearMarginRightPx = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the margin_bottom_px field.
+ * @return {?number} The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.getMarginBottomPx = function() {
+  return /** @type {?number} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the margin_bottom_px field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.getMarginBottomPxOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the margin_bottom_px field.
+ * @param {number} value The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.setMarginBottomPx = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the margin_bottom_px field has a value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.hasMarginBottomPx = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the margin_bottom_px field.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.marginBottomPxCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the margin_bottom_px field.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.clearMarginBottomPx = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the margin_top_px field.
+ * @return {?number} The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.getMarginTopPx = function() {
+  return /** @type {?number} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the margin_top_px field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.getMarginTopPxOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the margin_top_px field.
+ * @param {number} value The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.setMarginTopPx = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the margin_top_px field has a value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.hasMarginTopPx = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the margin_top_px field.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.marginTopPxCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the margin_top_px field.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.clearMarginTopPx = function() {
+  this.clear$Field(4);
+};
+
+
+/**
+ * Gets the value of the fraction_padding field.
+ * @return {?number} The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.getFractionPadding = function() {
+  return /** @type {?number} */ (this.get$Value(5));
+};
+
+
+/**
+ * Gets the value of the fraction_padding field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.getFractionPaddingOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(5));
+};
+
+
+/**
+ * Sets the value of the fraction_padding field.
+ * @param {number} value The value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.setFractionPadding = function(value) {
+  this.set$Value(5, value);
+};
+
+
+/**
+ * @return {boolean} Whether the fraction_padding field has a value.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.hasFractionPadding = function() {
+  return this.has$Value(5);
+};
+
+
+/**
+ * @return {number} The number of values in the fraction_padding field.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.fractionPaddingCount = function() {
+  return this.count$Values(5);
+};
+
+
+/**
+ * Clears the values in the fraction_padding field.
+ */
+sketchology.proto.CameraBoundsConfig.prototype.clearFractionPadding = function() {
+  this.clear$Field(5);
+};
+
+
+
+/**
+ * Message ImageInfo.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ImageInfo = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ImageInfo, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ImageInfo.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ImageInfo} The cloned message.
+ * @override
+ */
+sketchology.proto.ImageInfo.prototype.clone;
+
+
+/**
+ * Gets the value of the uri field.
+ * @return {?string} The value.
+ */
+sketchology.proto.ImageInfo.prototype.getUri = function() {
+  return /** @type {?string} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the uri field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.ImageInfo.prototype.getUriOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the uri field.
+ * @param {string} value The value.
+ */
+sketchology.proto.ImageInfo.prototype.setUri = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uri field has a value.
+ */
+sketchology.proto.ImageInfo.prototype.hasUri = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uri field.
+ */
+sketchology.proto.ImageInfo.prototype.uriCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uri field.
+ */
+sketchology.proto.ImageInfo.prototype.clearUri = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the asset_type field.
+ * @return {?sketchology.proto.ImageInfo.AssetType} The value.
+ */
+sketchology.proto.ImageInfo.prototype.getAssetType = function() {
+  return /** @type {?sketchology.proto.ImageInfo.AssetType} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the asset_type field or the default value if not set.
+ * @return {!sketchology.proto.ImageInfo.AssetType} The value.
+ */
+sketchology.proto.ImageInfo.prototype.getAssetTypeOrDefault = function() {
+  return /** @type {!sketchology.proto.ImageInfo.AssetType} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the asset_type field.
+ * @param {!sketchology.proto.ImageInfo.AssetType} value The value.
+ */
+sketchology.proto.ImageInfo.prototype.setAssetType = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the asset_type field has a value.
+ */
+sketchology.proto.ImageInfo.prototype.hasAssetType = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the asset_type field.
+ */
+sketchology.proto.ImageInfo.prototype.assetTypeCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the asset_type field.
+ */
+sketchology.proto.ImageInfo.prototype.clearAssetType = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Enumeration AssetType.
+ * @enum {number}
+ */
+sketchology.proto.ImageInfo.AssetType = {
+  DEFAULT: 0,
+  BORDER: 1,
+  STICKER: 2,
+  GRID: 3
+};
+
+
+
+/**
+ * Message ImageRect.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ImageRect = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ImageRect, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ImageRect.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ImageRect} The cloned message.
+ * @override
+ */
+sketchology.proto.ImageRect.prototype.clone;
+
+
+/**
+ * Gets the value of the rect field.
+ * @return {?sketchology.proto.Rect} The value.
+ */
+sketchology.proto.ImageRect.prototype.getRect = function() {
+  return /** @type {?sketchology.proto.Rect} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the rect field or the default value if not set.
+ * @return {!sketchology.proto.Rect} The value.
+ */
+sketchology.proto.ImageRect.prototype.getRectOrDefault = function() {
+  return /** @type {!sketchology.proto.Rect} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the rect field.
+ * @param {!sketchology.proto.Rect} value The value.
+ */
+sketchology.proto.ImageRect.prototype.setRect = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the rect field has a value.
+ */
+sketchology.proto.ImageRect.prototype.hasRect = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the rect field.
+ */
+sketchology.proto.ImageRect.prototype.rectCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the rect field.
+ */
+sketchology.proto.ImageRect.prototype.clearRect = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the bitmap_uri field.
+ * @return {?string} The value.
+ */
+sketchology.proto.ImageRect.prototype.getBitmapUri = function() {
+  return /** @type {?string} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the bitmap_uri field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.ImageRect.prototype.getBitmapUriOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the bitmap_uri field.
+ * @param {string} value The value.
+ */
+sketchology.proto.ImageRect.prototype.setBitmapUri = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the bitmap_uri field has a value.
+ */
+sketchology.proto.ImageRect.prototype.hasBitmapUri = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the bitmap_uri field.
+ */
+sketchology.proto.ImageRect.prototype.bitmapUriCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the bitmap_uri field.
+ */
+sketchology.proto.ImageRect.prototype.clearBitmapUri = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the attributes field.
+ * @return {?sketchology.proto.ElementAttributes} The value.
+ */
+sketchology.proto.ImageRect.prototype.getAttributes = function() {
+  return /** @type {?sketchology.proto.ElementAttributes} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the attributes field or the default value if not set.
+ * @return {!sketchology.proto.ElementAttributes} The value.
+ */
+sketchology.proto.ImageRect.prototype.getAttributesOrDefault = function() {
+  return /** @type {!sketchology.proto.ElementAttributes} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the attributes field.
+ * @param {!sketchology.proto.ElementAttributes} value The value.
+ */
+sketchology.proto.ImageRect.prototype.setAttributes = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the attributes field has a value.
+ */
+sketchology.proto.ImageRect.prototype.hasAttributes = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the attributes field.
+ */
+sketchology.proto.ImageRect.prototype.attributesCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the attributes field.
+ */
+sketchology.proto.ImageRect.prototype.clearAttributes = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the rotation_radians field.
+ * @return {?number} The value.
+ */
+sketchology.proto.ImageRect.prototype.getRotationRadians = function() {
+  return /** @type {?number} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the rotation_radians field or the default value if not set.
+ * @return {number} The value.
+ */
+sketchology.proto.ImageRect.prototype.getRotationRadiansOrDefault = function() {
+  return /** @type {number} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the rotation_radians field.
+ * @param {number} value The value.
+ */
+sketchology.proto.ImageRect.prototype.setRotationRadians = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the rotation_radians field has a value.
+ */
+sketchology.proto.ImageRect.prototype.hasRotationRadians = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the rotation_radians field.
+ */
+sketchology.proto.ImageRect.prototype.rotationRadiansCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the rotation_radians field.
+ */
+sketchology.proto.ImageRect.prototype.clearRotationRadians = function() {
+  this.clear$Field(4);
+};
+
+
+
+/**
+ * Message GridInfo.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.GridInfo = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.GridInfo, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.GridInfo.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.GridInfo} The cloned message.
+ * @override
+ */
+sketchology.proto.GridInfo.prototype.clone;
+
+
+/**
+ * Gets the value of the uri field.
+ * @return {?string} The value.
+ */
+sketchology.proto.GridInfo.prototype.getUri = function() {
+  return /** @type {?string} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the uri field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.GridInfo.prototype.getUriOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the uri field.
+ * @param {string} value The value.
+ */
+sketchology.proto.GridInfo.prototype.setUri = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uri field has a value.
+ */
+sketchology.proto.GridInfo.prototype.hasUri = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uri field.
+ */
+sketchology.proto.GridInfo.prototype.uriCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uri field.
+ */
+sketchology.proto.GridInfo.prototype.clearUri = function() {
+  this.clear$Field(1);
+};
+
+
+
+/**
+ * Message CreateDocument.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.CreateDocument = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.CreateDocument, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.CreateDocument.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.CreateDocument} The cloned message.
+ * @override
+ */
+sketchology.proto.CreateDocument.prototype.clone;
+
+
+/**
+ * Gets the value of the document_type field.
+ * @return {?sketchology.proto.DocumentType} The value.
+ */
+sketchology.proto.CreateDocument.prototype.getDocumentType = function() {
+  return /** @type {?sketchology.proto.DocumentType} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the document_type field or the default value if not set.
+ * @return {!sketchology.proto.DocumentType} The value.
+ */
+sketchology.proto.CreateDocument.prototype.getDocumentTypeOrDefault = function() {
+  return /** @type {!sketchology.proto.DocumentType} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the document_type field.
+ * @param {!sketchology.proto.DocumentType} value The value.
+ */
+sketchology.proto.CreateDocument.prototype.setDocumentType = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the document_type field has a value.
+ */
+sketchology.proto.CreateDocument.prototype.hasDocumentType = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the document_type field.
+ */
+sketchology.proto.CreateDocument.prototype.documentTypeCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the document_type field.
+ */
+sketchology.proto.CreateDocument.prototype.clearDocumentType = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the storage_type field.
+ * @return {?sketchology.proto.StorageType} The value.
+ */
+sketchology.proto.CreateDocument.prototype.getStorageType = function() {
+  return /** @type {?sketchology.proto.StorageType} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the storage_type field or the default value if not set.
+ * @return {!sketchology.proto.StorageType} The value.
+ */
+sketchology.proto.CreateDocument.prototype.getStorageTypeOrDefault = function() {
+  return /** @type {!sketchology.proto.StorageType} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the storage_type field.
+ * @param {!sketchology.proto.StorageType} value The value.
+ */
+sketchology.proto.CreateDocument.prototype.setStorageType = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the storage_type field has a value.
+ */
+sketchology.proto.CreateDocument.prototype.hasStorageType = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the storage_type field.
+ */
+sketchology.proto.CreateDocument.prototype.storageTypeCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the storage_type field.
+ */
+sketchology.proto.CreateDocument.prototype.clearStorageType = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the storage_path field.
+ * @return {?string} The value.
+ */
+sketchology.proto.CreateDocument.prototype.getStoragePath = function() {
+  return /** @type {?string} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the storage_path field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.CreateDocument.prototype.getStoragePathOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the storage_path field.
+ * @param {string} value The value.
+ */
+sketchology.proto.CreateDocument.prototype.setStoragePath = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the storage_path field has a value.
+ */
+sketchology.proto.CreateDocument.prototype.hasStoragePath = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the storage_path field.
+ */
+sketchology.proto.CreateDocument.prototype.storagePathCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the storage_path field.
+ */
+sketchology.proto.CreateDocument.prototype.clearStoragePath = function() {
+  this.clear$Field(3);
+};
+
+
+/**
+ * Gets the value of the snapshot field.
+ * @return {?sketchology.proto.Snapshot} The value.
+ */
+sketchology.proto.CreateDocument.prototype.getSnapshot = function() {
+  return /** @type {?sketchology.proto.Snapshot} */ (this.get$Value(4));
+};
+
+
+/**
+ * Gets the value of the snapshot field or the default value if not set.
+ * @return {!sketchology.proto.Snapshot} The value.
+ */
+sketchology.proto.CreateDocument.prototype.getSnapshotOrDefault = function() {
+  return /** @type {!sketchology.proto.Snapshot} */ (this.get$ValueOrDefault(4));
+};
+
+
+/**
+ * Sets the value of the snapshot field.
+ * @param {!sketchology.proto.Snapshot} value The value.
+ */
+sketchology.proto.CreateDocument.prototype.setSnapshot = function(value) {
+  this.set$Value(4, value);
+};
+
+
+/**
+ * @return {boolean} Whether the snapshot field has a value.
+ */
+sketchology.proto.CreateDocument.prototype.hasSnapshot = function() {
+  return this.has$Value(4);
+};
+
+
+/**
+ * @return {number} The number of values in the snapshot field.
+ */
+sketchology.proto.CreateDocument.prototype.snapshotCount = function() {
+  return this.count$Values(4);
+};
+
+
+/**
+ * Clears the values in the snapshot field.
+ */
+sketchology.proto.CreateDocument.prototype.clearSnapshot = function() {
+  this.clear$Field(4);
+};
+
+
+
+/**
+ * Message AddPath.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.AddPath = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.AddPath, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.AddPath.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.AddPath} The cloned message.
+ * @override
+ */
+sketchology.proto.AddPath.prototype.clone;
+
+
+/**
+ * Gets the value of the path field.
+ * @return {?sketchology.proto.Path} The value.
+ */
+sketchology.proto.AddPath.prototype.getPath = function() {
+  return /** @type {?sketchology.proto.Path} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the path field or the default value if not set.
+ * @return {!sketchology.proto.Path} The value.
+ */
+sketchology.proto.AddPath.prototype.getPathOrDefault = function() {
+  return /** @type {!sketchology.proto.Path} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the path field.
+ * @param {!sketchology.proto.Path} value The value.
+ */
+sketchology.proto.AddPath.prototype.setPath = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the path field has a value.
+ */
+sketchology.proto.AddPath.prototype.hasPath = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the path field.
+ */
+sketchology.proto.AddPath.prototype.pathCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the path field.
+ */
+sketchology.proto.AddPath.prototype.clearPath = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the uuid field.
+ * @return {?string} The value.
+ */
+sketchology.proto.AddPath.prototype.getUuid = function() {
+  return /** @type {?string} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the uuid field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.AddPath.prototype.getUuidOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the uuid field.
+ * @param {string} value The value.
+ */
+sketchology.proto.AddPath.prototype.setUuid = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uuid field has a value.
+ */
+sketchology.proto.AddPath.prototype.hasUuid = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the uuid field.
+ */
+sketchology.proto.AddPath.prototype.uuidCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the uuid field.
+ */
+sketchology.proto.AddPath.prototype.clearUuid = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message PusherPositionUpdate.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.PusherPositionUpdate = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.PusherPositionUpdate, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.PusherPositionUpdate.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.PusherPositionUpdate} The cloned message.
+ * @override
+ */
+sketchology.proto.PusherPositionUpdate.prototype.clone;
+
+
+/**
+ * Gets the value of the uuid field.
+ * @return {?string} The value.
+ */
+sketchology.proto.PusherPositionUpdate.prototype.getUuid = function() {
+  return /** @type {?string} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the uuid field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.PusherPositionUpdate.prototype.getUuidOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the uuid field.
+ * @param {string} value The value.
+ */
+sketchology.proto.PusherPositionUpdate.prototype.setUuid = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uuid field has a value.
+ */
+sketchology.proto.PusherPositionUpdate.prototype.hasUuid = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uuid field.
+ */
+sketchology.proto.PusherPositionUpdate.prototype.uuidCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uuid field.
+ */
+sketchology.proto.PusherPositionUpdate.prototype.clearUuid = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the pointer_location field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.Point} The value.
+ */
+sketchology.proto.PusherPositionUpdate.prototype.getPointerLocation = function(index) {
+  return /** @type {?sketchology.proto.Point} */ (this.get$Value(2, index));
+};
+
+
+/**
+ * Gets the value of the pointer_location field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.Point} The value.
+ */
+sketchology.proto.PusherPositionUpdate.prototype.getPointerLocationOrDefault = function(index) {
+  return /** @type {!sketchology.proto.Point} */ (this.get$ValueOrDefault(2, index));
+};
+
+
+/**
+ * Adds a value to the pointer_location field.
+ * @param {!sketchology.proto.Point} value The value to add.
+ */
+sketchology.proto.PusherPositionUpdate.prototype.addPointerLocation = function(value) {
+  this.add$Value(2, value);
+};
+
+
+/**
+ * Returns the array of values in the pointer_location field.
+ * @return {!Array<!sketchology.proto.Point>} The values in the field.
+ */
+sketchology.proto.PusherPositionUpdate.prototype.pointerLocationArray = function() {
+  return /** @type {!Array<!sketchology.proto.Point>} */ (this.array$Values(2));
+};
+
+
+/**
+ * @return {boolean} Whether the pointer_location field has a value.
+ */
+sketchology.proto.PusherPositionUpdate.prototype.hasPointerLocation = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the pointer_location field.
+ */
+sketchology.proto.PusherPositionUpdate.prototype.pointerLocationCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the pointer_location field.
+ */
+sketchology.proto.PusherPositionUpdate.prototype.clearPointerLocation = function() {
+  this.clear$Field(2);
+};
+
+
+
+/**
+ * Message ElementQueryData.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ElementQueryData = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ElementQueryData, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ElementQueryData.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ElementQueryData} The cloned message.
+ * @override
+ */
+sketchology.proto.ElementQueryData.prototype.clone;
+
+
+/**
+ * Gets the value of the item field at the index given.
+ * @param {number} index The index to lookup.
+ * @return {?sketchology.proto.ElementQueryItem} The value.
+ */
+sketchology.proto.ElementQueryData.prototype.getItem = function(index) {
+  return /** @type {?sketchology.proto.ElementQueryItem} */ (this.get$Value(1, index));
+};
+
+
+/**
+ * Gets the value of the item field at the index given or the default value if not set.
+ * @param {number} index The index to lookup.
+ * @return {!sketchology.proto.ElementQueryItem} The value.
+ */
+sketchology.proto.ElementQueryData.prototype.getItemOrDefault = function(index) {
+  return /** @type {!sketchology.proto.ElementQueryItem} */ (this.get$ValueOrDefault(1, index));
+};
+
+
+/**
+ * Adds a value to the item field.
+ * @param {!sketchology.proto.ElementQueryItem} value The value to add.
+ */
+sketchology.proto.ElementQueryData.prototype.addItem = function(value) {
+  this.add$Value(1, value);
+};
+
+
+/**
+ * Returns the array of values in the item field.
+ * @return {!Array<!sketchology.proto.ElementQueryItem>} The values in the field.
+ */
+sketchology.proto.ElementQueryData.prototype.itemArray = function() {
+  return /** @type {!Array<!sketchology.proto.ElementQueryItem>} */ (this.array$Values(1));
+};
+
+
+/**
+ * @return {boolean} Whether the item field has a value.
+ */
+sketchology.proto.ElementQueryData.prototype.hasItem = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the item field.
+ */
+sketchology.proto.ElementQueryData.prototype.itemCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the item field.
+ */
+sketchology.proto.ElementQueryData.prototype.clearItem = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the up_world_location field.
+ * @return {?sketchology.proto.Point} The value.
+ */
+sketchology.proto.ElementQueryData.prototype.getUpWorldLocation = function() {
+  return /** @type {?sketchology.proto.Point} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the up_world_location field or the default value if not set.
+ * @return {!sketchology.proto.Point} The value.
+ */
+sketchology.proto.ElementQueryData.prototype.getUpWorldLocationOrDefault = function() {
+  return /** @type {!sketchology.proto.Point} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the up_world_location field.
+ * @param {!sketchology.proto.Point} value The value.
+ */
+sketchology.proto.ElementQueryData.prototype.setUpWorldLocation = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the up_world_location field has a value.
+ */
+sketchology.proto.ElementQueryData.prototype.hasUpWorldLocation = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the up_world_location field.
+ */
+sketchology.proto.ElementQueryData.prototype.upWorldLocationCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the up_world_location field.
+ */
+sketchology.proto.ElementQueryData.prototype.clearUpWorldLocation = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the down_world_location field.
+ * @return {?sketchology.proto.Point} The value.
+ */
+sketchology.proto.ElementQueryData.prototype.getDownWorldLocation = function() {
+  return /** @type {?sketchology.proto.Point} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the down_world_location field or the default value if not set.
+ * @return {!sketchology.proto.Point} The value.
+ */
+sketchology.proto.ElementQueryData.prototype.getDownWorldLocationOrDefault = function() {
+  return /** @type {!sketchology.proto.Point} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the down_world_location field.
+ * @param {!sketchology.proto.Point} value The value.
+ */
+sketchology.proto.ElementQueryData.prototype.setDownWorldLocation = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the down_world_location field has a value.
+ */
+sketchology.proto.ElementQueryData.prototype.hasDownWorldLocation = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the down_world_location field.
+ */
+sketchology.proto.ElementQueryData.prototype.downWorldLocationCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the down_world_location field.
+ */
+sketchology.proto.ElementQueryData.prototype.clearDownWorldLocation = function() {
+  this.clear$Field(3);
+};
+
+
+
+/**
+ * Message ElementQueryItem.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ElementQueryItem = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ElementQueryItem, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ElementQueryItem.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ElementQueryItem} The cloned message.
+ * @override
+ */
+sketchology.proto.ElementQueryItem.prototype.clone;
+
+
+/**
+ * Gets the value of the uuid field.
+ * @return {?string} The value.
+ */
+sketchology.proto.ElementQueryItem.prototype.getUuid = function() {
+  return /** @type {?string} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the uuid field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.ElementQueryItem.prototype.getUuidOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the uuid field.
+ * @param {string} value The value.
+ */
+sketchology.proto.ElementQueryItem.prototype.setUuid = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uuid field has a value.
+ */
+sketchology.proto.ElementQueryItem.prototype.hasUuid = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the uuid field.
+ */
+sketchology.proto.ElementQueryItem.prototype.uuidCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the uuid field.
+ */
+sketchology.proto.ElementQueryItem.prototype.clearUuid = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the world_bounds field.
+ * @return {?sketchology.proto.Rect} The value.
+ */
+sketchology.proto.ElementQueryItem.prototype.getWorldBounds = function() {
+  return /** @type {?sketchology.proto.Rect} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the world_bounds field or the default value if not set.
+ * @return {!sketchology.proto.Rect} The value.
+ */
+sketchology.proto.ElementQueryItem.prototype.getWorldBoundsOrDefault = function() {
+  return /** @type {!sketchology.proto.Rect} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the world_bounds field.
+ * @param {!sketchology.proto.Rect} value The value.
+ */
+sketchology.proto.ElementQueryItem.prototype.setWorldBounds = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the world_bounds field has a value.
+ */
+sketchology.proto.ElementQueryItem.prototype.hasWorldBounds = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the world_bounds field.
+ */
+sketchology.proto.ElementQueryItem.prototype.worldBoundsCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the world_bounds field.
+ */
+sketchology.proto.ElementQueryItem.prototype.clearWorldBounds = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the uri field.
+ * @return {?string} The value.
+ */
+sketchology.proto.ElementQueryItem.prototype.getUri = function() {
+  return /** @type {?string} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the uri field or the default value if not set.
+ * @return {string} The value.
+ */
+sketchology.proto.ElementQueryItem.prototype.getUriOrDefault = function() {
+  return /** @type {string} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the uri field.
+ * @param {string} value The value.
+ */
+sketchology.proto.ElementQueryItem.prototype.setUri = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the uri field has a value.
+ */
+sketchology.proto.ElementQueryItem.prototype.hasUri = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the uri field.
+ */
+sketchology.proto.ElementQueryItem.prototype.uriCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the uri field.
+ */
+sketchology.proto.ElementQueryItem.prototype.clearUri = function() {
+  this.clear$Field(3);
+};
+
+
+
+/**
+ * Message SelectionState.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.SelectionState = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.SelectionState, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.SelectionState.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.SelectionState} The cloned message.
+ * @override
+ */
+sketchology.proto.SelectionState.prototype.clone;
+
+
+/**
+ * Gets the value of the anything_selected field.
+ * @return {?boolean} The value.
+ */
+sketchology.proto.SelectionState.prototype.getAnythingSelected = function() {
+  return /** @type {?boolean} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the anything_selected field or the default value if not set.
+ * @return {boolean} The value.
+ */
+sketchology.proto.SelectionState.prototype.getAnythingSelectedOrDefault = function() {
+  return /** @type {boolean} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the anything_selected field.
+ * @param {boolean} value The value.
+ */
+sketchology.proto.SelectionState.prototype.setAnythingSelected = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the anything_selected field has a value.
+ */
+sketchology.proto.SelectionState.prototype.hasAnythingSelected = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the anything_selected field.
+ */
+sketchology.proto.SelectionState.prototype.anythingSelectedCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the anything_selected field.
+ */
+sketchology.proto.SelectionState.prototype.clearAnythingSelected = function() {
+  this.clear$Field(1);
+};
+
+
+
+/**
+ * Message ToolEvent.
+ * @constructor
+ * @extends {goog.proto2.Message}
+ * @final
+ */
+sketchology.proto.ToolEvent = function() {
+  goog.proto2.Message.call(this);
+};
+goog.inherits(sketchology.proto.ToolEvent, goog.proto2.Message);
+
+
+/**
+ * Descriptor for this message, deserialized lazily in getDescriptor().
+ * @private {?goog.proto2.Descriptor}
+ */
+sketchology.proto.ToolEvent.descriptor_ = null;
+
+
+/**
+ * Overrides {@link goog.proto2.Message#clone} to specify its exact return type.
+ * @return {!sketchology.proto.ToolEvent} The cloned message.
+ * @override
+ */
+sketchology.proto.ToolEvent.prototype.clone;
+
+
+/**
+ * Gets the value of the pusher_position_update field.
+ * @return {?sketchology.proto.PusherPositionUpdate} The value.
+ */
+sketchology.proto.ToolEvent.prototype.getPusherPositionUpdate = function() {
+  return /** @type {?sketchology.proto.PusherPositionUpdate} */ (this.get$Value(1));
+};
+
+
+/**
+ * Gets the value of the pusher_position_update field or the default value if not set.
+ * @return {!sketchology.proto.PusherPositionUpdate} The value.
+ */
+sketchology.proto.ToolEvent.prototype.getPusherPositionUpdateOrDefault = function() {
+  return /** @type {!sketchology.proto.PusherPositionUpdate} */ (this.get$ValueOrDefault(1));
+};
+
+
+/**
+ * Sets the value of the pusher_position_update field.
+ * @param {!sketchology.proto.PusherPositionUpdate} value The value.
+ */
+sketchology.proto.ToolEvent.prototype.setPusherPositionUpdate = function(value) {
+  this.set$Value(1, value);
+};
+
+
+/**
+ * @return {boolean} Whether the pusher_position_update field has a value.
+ */
+sketchology.proto.ToolEvent.prototype.hasPusherPositionUpdate = function() {
+  return this.has$Value(1);
+};
+
+
+/**
+ * @return {number} The number of values in the pusher_position_update field.
+ */
+sketchology.proto.ToolEvent.prototype.pusherPositionUpdateCount = function() {
+  return this.count$Values(1);
+};
+
+
+/**
+ * Clears the values in the pusher_position_update field.
+ */
+sketchology.proto.ToolEvent.prototype.clearPusherPositionUpdate = function() {
+  this.clear$Field(1);
+};
+
+
+/**
+ * Gets the value of the element_query_data field.
+ * @return {?sketchology.proto.ElementQueryData} The value.
+ */
+sketchology.proto.ToolEvent.prototype.getElementQueryData = function() {
+  return /** @type {?sketchology.proto.ElementQueryData} */ (this.get$Value(2));
+};
+
+
+/**
+ * Gets the value of the element_query_data field or the default value if not set.
+ * @return {!sketchology.proto.ElementQueryData} The value.
+ */
+sketchology.proto.ToolEvent.prototype.getElementQueryDataOrDefault = function() {
+  return /** @type {!sketchology.proto.ElementQueryData} */ (this.get$ValueOrDefault(2));
+};
+
+
+/**
+ * Sets the value of the element_query_data field.
+ * @param {!sketchology.proto.ElementQueryData} value The value.
+ */
+sketchology.proto.ToolEvent.prototype.setElementQueryData = function(value) {
+  this.set$Value(2, value);
+};
+
+
+/**
+ * @return {boolean} Whether the element_query_data field has a value.
+ */
+sketchology.proto.ToolEvent.prototype.hasElementQueryData = function() {
+  return this.has$Value(2);
+};
+
+
+/**
+ * @return {number} The number of values in the element_query_data field.
+ */
+sketchology.proto.ToolEvent.prototype.elementQueryDataCount = function() {
+  return this.count$Values(2);
+};
+
+
+/**
+ * Clears the values in the element_query_data field.
+ */
+sketchology.proto.ToolEvent.prototype.clearElementQueryData = function() {
+  this.clear$Field(2);
+};
+
+
+/**
+ * Gets the value of the selection_state field.
+ * @return {?sketchology.proto.SelectionState} The value.
+ */
+sketchology.proto.ToolEvent.prototype.getSelectionState = function() {
+  return /** @type {?sketchology.proto.SelectionState} */ (this.get$Value(3));
+};
+
+
+/**
+ * Gets the value of the selection_state field or the default value if not set.
+ * @return {!sketchology.proto.SelectionState} The value.
+ */
+sketchology.proto.ToolEvent.prototype.getSelectionStateOrDefault = function() {
+  return /** @type {!sketchology.proto.SelectionState} */ (this.get$ValueOrDefault(3));
+};
+
+
+/**
+ * Sets the value of the selection_state field.
+ * @param {!sketchology.proto.SelectionState} value The value.
+ */
+sketchology.proto.ToolEvent.prototype.setSelectionState = function(value) {
+  this.set$Value(3, value);
+};
+
+
+/**
+ * @return {boolean} Whether the selection_state field has a value.
+ */
+sketchology.proto.ToolEvent.prototype.hasSelectionState = function() {
+  return this.has$Value(3);
+};
+
+
+/**
+ * @return {number} The number of values in the selection_state field.
+ */
+sketchology.proto.ToolEvent.prototype.selectionStateCount = function() {
+  return this.count$Values(3);
+};
+
+
+/**
+ * Clears the values in the selection_state field.
+ */
+sketchology.proto.ToolEvent.prototype.clearSelectionState = function() {
+  this.clear$Field(3);
+};
+
+
+/** @override */
+sketchology.proto.Command.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.Command.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'Command',
+        fullName: 'sketchology.proto.Command'
+      },
+      1: {
+        name: 'set_viewport',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Viewport
+      },
+      2: {
+        name: 'tool_params',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ToolParams
+      },
+      3: {
+        name: 'add_path',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.AddPath
+      },
+      4: {
+        name: 'camera_position',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Rect
+      },
+      5: {
+        name: 'page_bounds',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Rect
+      },
+      6: {
+        name: 'image_export',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ImageExport
+      },
+      7: {
+        name: 'flag_assignment',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.FlagAssignment
+      },
+      8: {
+        name: 'set_element_transforms',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ElementMutation
+      },
+      9: {
+        name: 'add_element',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.AddElement
+      },
+      10: {
+        name: 'background_image',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.BackgroundImageInfo
+      },
+      11: {
+        name: 'background_color',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.BackgroundColor
+      },
+      12: {
+        name: 'set_out_of_bounds_color',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.OutOfBoundsColor
+      },
+      13: {
+        name: 'set_page_border',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Border
+      },
+      14: {
+        name: 'send_input_stream',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.SInputStream
+      },
+      15: {
+        name: 'sequence_point',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.SequencePoint
+      },
+      16: {
+        name: 'set_callback_flags',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.SetCallbackFlags
+      },
+      17: {
+        name: 'set_camera_bounds_config',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.CameraBoundsConfig
+      },
+      18: {
+        name: 'deselect_all',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.NoArgCommand
+      },
+      19: {
+        name: 'add_image_rect',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ImageRect
+      },
+      21: {
+        name: 'clear',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.NoArgCommand
+      },
+      22: {
+        name: 'remove_all_elements',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.NoArgCommand
+      },
+      23: {
+        name: 'undo',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.NoArgCommand
+      },
+      24: {
+        name: 'redo',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.NoArgCommand
+      },
+      25: {
+        name: 'evict_image_data',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.EvictImageData
+      },
+      26: {
+        name: 'replace_elements',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ReplaceElementsCommand
+      },
+      27: {
+        name: 'commit_crop',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.NoArgCommand
+      },
+      28: {
+        name: 'element_animation',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ElementAnimation
+      },
+      29: {
+        name: 'set_grid',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.GridInfo
+      },
+      30: {
+        name: 'clear_grid',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.NoArgCommand
+      }
+    };
+    sketchology.proto.Command.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.Command, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.Command.getDescriptor =
+    sketchology.proto.Command.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.CommandList.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.CommandList.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'CommandList',
+        fullName: 'sketchology.proto.CommandList'
+      },
+      1: {
+        name: 'commands',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Command
+      }
+    };
+    sketchology.proto.CommandList.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.CommandList, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.CommandList.getDescriptor =
+    sketchology.proto.CommandList.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.NoArgCommand.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.NoArgCommand.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'NoArgCommand',
+        fullName: 'sketchology.proto.NoArgCommand'
+      }
+    };
+    sketchology.proto.NoArgCommand.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.NoArgCommand, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.NoArgCommand.getDescriptor =
+    sketchology.proto.NoArgCommand.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ReplaceElementsCommand.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ReplaceElementsCommand.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ReplaceElementsCommand',
+        fullName: 'sketchology.proto.ReplaceElementsCommand'
+      },
+      1: {
+        name: 'uuids_to_remove',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      2: {
+        name: 'paths_to_add',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Path
+      }
+    };
+    sketchology.proto.ReplaceElementsCommand.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ReplaceElementsCommand, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ReplaceElementsCommand.getDescriptor =
+    sketchology.proto.ReplaceElementsCommand.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.EvictImageData.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.EvictImageData.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'EvictImageData',
+        fullName: 'sketchology.proto.EvictImageData'
+      },
+      1: {
+        name: 'uri',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      }
+    };
+    sketchology.proto.EvictImageData.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.EvictImageData, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.EvictImageData.getDescriptor =
+    sketchology.proto.EvictImageData.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.Viewport.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.Viewport.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'Viewport',
+        fullName: 'sketchology.proto.Viewport'
+      },
+      1: {
+        name: 'fbo_handle',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      },
+      2: {
+        name: 'width',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      },
+      3: {
+        name: 'height',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      },
+      4: {
+        name: 'ppi',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      }
+    };
+    sketchology.proto.Viewport.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.Viewport, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.Viewport.getDescriptor =
+    sketchology.proto.Viewport.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ImageExport.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ImageExport.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ImageExport',
+        fullName: 'sketchology.proto.ImageExport'
+      },
+      1: {
+        name: 'max_dimension_px',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        defaultValue: 1024,
+        type: Number
+      },
+      2: {
+        name: 'should_draw_background',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        defaultValue: true,
+        type: Boolean
+      }
+    };
+    sketchology.proto.ImageExport.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ImageExport, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ImageExport.getDescriptor =
+    sketchology.proto.ImageExport.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.LinearPathAnimation.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.LinearPathAnimation.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'LinearPathAnimation',
+        fullName: 'sketchology.proto.LinearPathAnimation'
+      },
+      1: {
+        name: 'rgba_from',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      },
+      2: {
+        name: 'rgba_seconds',
+        fieldType: goog.proto2.Message.FieldType.DOUBLE,
+        type: Number
+      },
+      3: {
+        name: 'dilation_from',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      4: {
+        name: 'dilation_seconds',
+        fieldType: goog.proto2.Message.FieldType.DOUBLE,
+        type: Number
+      }
+    };
+    sketchology.proto.LinearPathAnimation.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.LinearPathAnimation, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.LinearPathAnimation.getDescriptor =
+    sketchology.proto.LinearPathAnimation.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.LineSize.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.LineSize.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'LineSize',
+        fullName: 'sketchology.proto.LineSize'
+      },
+      7: {
+        name: 'stroke_width',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      8: {
+        name: 'units',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: sketchology.proto.LineSize.SizeType.WORLD_UNITS,
+        type: sketchology.proto.LineSize.SizeType
+      }
+    };
+    sketchology.proto.LineSize.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.LineSize, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.LineSize.getDescriptor =
+    sketchology.proto.LineSize.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.PusherToolParams.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.PusherToolParams.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'PusherToolParams',
+        fullName: 'sketchology.proto.PusherToolParams'
+      },
+      1: {
+        name: 'manipulate_stickers',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        type: Boolean
+      },
+      2: {
+        name: 'manipulate_text',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        type: Boolean
+      }
+    };
+    sketchology.proto.PusherToolParams.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.PusherToolParams, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.PusherToolParams.getDescriptor =
+    sketchology.proto.PusherToolParams.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ToolParams.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ToolParams.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ToolParams',
+        fullName: 'sketchology.proto.ToolParams'
+      },
+      1: {
+        name: 'tool',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: sketchology.proto.ToolParams.ToolType.UNKNOWN,
+        type: sketchology.proto.ToolParams.ToolType
+      },
+      2: {
+        name: 'rgba',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      },
+      3: {
+        name: 'line_size',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.LineSize
+      },
+      4: {
+        name: 'pusher_tool_params',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.PusherToolParams
+      },
+      5: {
+        name: 'brush_type',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: sketchology.proto.BrushType.UNKNOWN_BRUSH,
+        type: sketchology.proto.BrushType
+      },
+      6: {
+        name: 'linear_path_animation',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.LinearPathAnimation
+      }
+    };
+    sketchology.proto.ToolParams.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ToolParams, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ToolParams.getDescriptor =
+    sketchology.proto.ToolParams.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.FlagAssignment.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.FlagAssignment.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'FlagAssignment',
+        fullName: 'sketchology.proto.FlagAssignment'
+      },
+      1: {
+        name: 'flag',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: sketchology.proto.Flag.UNKNOWN,
+        type: sketchology.proto.Flag
+      },
+      2: {
+        name: 'bool_value',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        type: Boolean
+      }
+    };
+    sketchology.proto.FlagAssignment.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.FlagAssignment, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.FlagAssignment.getDescriptor =
+    sketchology.proto.FlagAssignment.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.AddElement.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.AddElement.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'AddElement',
+        fullName: 'sketchology.proto.AddElement'
+      },
+      1: {
+        name: 'bundle',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ElementBundle
+      },
+      2: {
+        name: 'below_element_with_uuid',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      }
+    };
+    sketchology.proto.AddElement.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.AddElement, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.AddElement.getDescriptor =
+    sketchology.proto.AddElement.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.OutOfBoundsColor.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.OutOfBoundsColor.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'OutOfBoundsColor',
+        fullName: 'sketchology.proto.OutOfBoundsColor'
+      },
+      1: {
+        name: 'rgba',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      }
+    };
+    sketchology.proto.OutOfBoundsColor.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.OutOfBoundsColor, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.OutOfBoundsColor.getDescriptor =
+    sketchology.proto.OutOfBoundsColor.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.SInputStream.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.SInputStream.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'SInputStream',
+        fullName: 'sketchology.proto.SInputStream'
+      },
+      1: {
+        name: 'screen_width',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      },
+      2: {
+        name: 'screen_height',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      },
+      3: {
+        name: 'screen_ppi',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      4: {
+        name: 'input',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.SInput
+      }
+    };
+    sketchology.proto.SInputStream.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.SInputStream, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.SInputStream.getDescriptor =
+    sketchology.proto.SInputStream.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.SInput.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.SInput.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'SInput',
+        fullName: 'sketchology.proto.SInput'
+      },
+      1: {
+        name: 'type',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: sketchology.proto.SInput.InputType.UNKNOWN,
+        type: sketchology.proto.SInput.InputType
+      },
+      2: {
+        name: 'id',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      },
+      3: {
+        name: 'flags',
+        fieldType: goog.proto2.Message.FieldType.UINT32,
+        type: Number
+      },
+      4: {
+        name: 'time_s',
+        fieldType: goog.proto2.Message.FieldType.DOUBLE,
+        type: Number
+      },
+      5: {
+        name: 'screen_pos_x',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      6: {
+        name: 'screen_pos_y',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      7: {
+        name: 'pressure',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      8: {
+        name: 'wheel_delta',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      9: {
+        name: 'tilt',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      10: {
+        name: 'orientation',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      }
+    };
+    sketchology.proto.SInput.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.SInput, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.SInput.getDescriptor =
+    sketchology.proto.SInput.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.SimulatedInput.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.SimulatedInput.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'SimulatedInput',
+        fullName: 'sketchology.proto.SimulatedInput'
+      },
+      1: {
+        name: 'xs',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      2: {
+        name: 'ys',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      3: {
+        name: 'ts_secs',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.DOUBLE,
+        type: Number
+      },
+      4: {
+        name: 'source_details',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.SourceDetails
+      }
+    };
+    sketchology.proto.SimulatedInput.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.SimulatedInput, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.SimulatedInput.getDescriptor =
+    sketchology.proto.SimulatedInput.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.SequencePoint.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.SequencePoint.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'SequencePoint',
+        fullName: 'sketchology.proto.SequencePoint'
+      },
+      1: {
+        name: 'id',
+        fieldType: goog.proto2.Message.FieldType.INT32,
+        type: Number
+      }
+    };
+    sketchology.proto.SequencePoint.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.SequencePoint, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.SequencePoint.getDescriptor =
+    sketchology.proto.SequencePoint.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.SetCallbackFlags.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.SetCallbackFlags.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'SetCallbackFlags',
+        fullName: 'sketchology.proto.SetCallbackFlags'
+      },
+      1: {
+        name: 'source_details',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.SourceDetails
+      },
+      2: {
+        name: 'callback_flags',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.CallbackFlags
+      }
+    };
+    sketchology.proto.SetCallbackFlags.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.SetCallbackFlags, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.SetCallbackFlags.getDescriptor =
+    sketchology.proto.SetCallbackFlags.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.EngineState.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.EngineState.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'EngineState',
+        fullName: 'sketchology.proto.EngineState'
+      },
+      1: {
+        name: 'camera_position',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Rect
+      },
+      2: {
+        name: 'page_bounds',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Rect
+      },
+      3: {
+        name: 'selection_is_live',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        type: Boolean
+      }
+    };
+    sketchology.proto.EngineState.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.EngineState, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.EngineState.getDescriptor =
+    sketchology.proto.EngineState.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.CameraBoundsConfig.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.CameraBoundsConfig.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'CameraBoundsConfig',
+        fullName: 'sketchology.proto.CameraBoundsConfig'
+      },
+      1: {
+        name: 'margin_left_px',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      2: {
+        name: 'margin_right_px',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      3: {
+        name: 'margin_bottom_px',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      4: {
+        name: 'margin_top_px',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      },
+      5: {
+        name: 'fraction_padding',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        defaultValue: 0.1,
+        type: Number
+      }
+    };
+    sketchology.proto.CameraBoundsConfig.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.CameraBoundsConfig, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.CameraBoundsConfig.getDescriptor =
+    sketchology.proto.CameraBoundsConfig.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ImageInfo.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ImageInfo.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ImageInfo',
+        fullName: 'sketchology.proto.ImageInfo'
+      },
+      1: {
+        name: 'uri',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      2: {
+        name: 'asset_type',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: sketchology.proto.ImageInfo.AssetType.DEFAULT,
+        type: sketchology.proto.ImageInfo.AssetType
+      }
+    };
+    sketchology.proto.ImageInfo.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ImageInfo, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ImageInfo.getDescriptor =
+    sketchology.proto.ImageInfo.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ImageRect.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ImageRect.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ImageRect',
+        fullName: 'sketchology.proto.ImageRect'
+      },
+      1: {
+        name: 'rect',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Rect
+      },
+      2: {
+        name: 'bitmap_uri',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      3: {
+        name: 'attributes',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ElementAttributes
+      },
+      4: {
+        name: 'rotation_radians',
+        fieldType: goog.proto2.Message.FieldType.FLOAT,
+        type: Number
+      }
+    };
+    sketchology.proto.ImageRect.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ImageRect, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ImageRect.getDescriptor =
+    sketchology.proto.ImageRect.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.GridInfo.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.GridInfo.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'GridInfo',
+        fullName: 'sketchology.proto.GridInfo'
+      },
+      1: {
+        name: 'uri',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      }
+    };
+    sketchology.proto.GridInfo.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.GridInfo, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.GridInfo.getDescriptor =
+    sketchology.proto.GridInfo.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.CreateDocument.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.CreateDocument.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'CreateDocument',
+        fullName: 'sketchology.proto.CreateDocument'
+      },
+      1: {
+        name: 'document_type',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: sketchology.proto.DocumentType.SINGLE_USER_DOCUMENT,
+        type: sketchology.proto.DocumentType
+      },
+      2: {
+        name: 'storage_type',
+        fieldType: goog.proto2.Message.FieldType.ENUM,
+        defaultValue: sketchology.proto.StorageType.IN_MEMORY_STORAGE,
+        type: sketchology.proto.StorageType
+      },
+      3: {
+        name: 'storage_path',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      4: {
+        name: 'snapshot',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Snapshot
+      }
+    };
+    sketchology.proto.CreateDocument.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.CreateDocument, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.CreateDocument.getDescriptor =
+    sketchology.proto.CreateDocument.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.AddPath.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.AddPath.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'AddPath',
+        fullName: 'sketchology.proto.AddPath'
+      },
+      1: {
+        name: 'path',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Path
+      },
+      2: {
+        name: 'uuid',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      }
+    };
+    sketchology.proto.AddPath.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.AddPath, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.AddPath.getDescriptor =
+    sketchology.proto.AddPath.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.PusherPositionUpdate.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.PusherPositionUpdate.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'PusherPositionUpdate',
+        fullName: 'sketchology.proto.PusherPositionUpdate'
+      },
+      1: {
+        name: 'uuid',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      2: {
+        name: 'pointer_location',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Point
+      }
+    };
+    sketchology.proto.PusherPositionUpdate.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.PusherPositionUpdate, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.PusherPositionUpdate.getDescriptor =
+    sketchology.proto.PusherPositionUpdate.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ElementQueryData.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ElementQueryData.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ElementQueryData',
+        fullName: 'sketchology.proto.ElementQueryData'
+      },
+      1: {
+        name: 'item',
+        repeated: true,
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ElementQueryItem
+      },
+      2: {
+        name: 'up_world_location',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Point
+      },
+      3: {
+        name: 'down_world_location',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Point
+      }
+    };
+    sketchology.proto.ElementQueryData.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ElementQueryData, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ElementQueryData.getDescriptor =
+    sketchology.proto.ElementQueryData.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ElementQueryItem.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ElementQueryItem.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ElementQueryItem',
+        fullName: 'sketchology.proto.ElementQueryItem'
+      },
+      1: {
+        name: 'uuid',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      },
+      2: {
+        name: 'world_bounds',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.Rect
+      },
+      3: {
+        name: 'uri',
+        fieldType: goog.proto2.Message.FieldType.STRING,
+        type: String
+      }
+    };
+    sketchology.proto.ElementQueryItem.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ElementQueryItem, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ElementQueryItem.getDescriptor =
+    sketchology.proto.ElementQueryItem.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.SelectionState.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.SelectionState.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'SelectionState',
+        fullName: 'sketchology.proto.SelectionState'
+      },
+      1: {
+        name: 'anything_selected',
+        fieldType: goog.proto2.Message.FieldType.BOOL,
+        type: Boolean
+      }
+    };
+    sketchology.proto.SelectionState.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.SelectionState, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.SelectionState.getDescriptor =
+    sketchology.proto.SelectionState.prototype.getDescriptor;
+
+
+/** @override */
+sketchology.proto.ToolEvent.prototype.getDescriptor = function() {
+  var descriptor = sketchology.proto.ToolEvent.descriptor_;
+  if (!descriptor) {
+    // The descriptor is created lazily when we instantiate a new instance.
+    var descriptorObj = {
+      0: {
+        name: 'ToolEvent',
+        fullName: 'sketchology.proto.ToolEvent'
+      },
+      1: {
+        name: 'pusher_position_update',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.PusherPositionUpdate
+      },
+      2: {
+        name: 'element_query_data',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.ElementQueryData
+      },
+      3: {
+        name: 'selection_state',
+        fieldType: goog.proto2.Message.FieldType.MESSAGE,
+        type: sketchology.proto.SelectionState
+      }
+    };
+    sketchology.proto.ToolEvent.descriptor_ = descriptor =
+        goog.proto2.Message.createDescriptor(
+             sketchology.proto.ToolEvent, descriptorObj);
+  }
+  return descriptor;
+};
+
+
+/** @nocollapse */
+sketchology.proto.ToolEvent.getDescriptor =
+    sketchology.proto.ToolEvent.prototype.getDescriptor;
diff --git a/third_party/ink/sketchology/public/js/common/brush_model.js b/third_party/ink/sketchology/public/js/common/brush_model.js
new file mode 100644
index 0000000..8ffbe4c0
--- /dev/null
+++ b/third_party/ink/sketchology/public/js/common/brush_model.js
@@ -0,0 +1,243 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+goog.provide('ink.BrushModel');
+
+goog.require('goog.events');
+goog.require('goog.events.EventTarget');
+goog.require('ink.Model');
+goog.require('sketchology.proto.BrushType');
+goog.require('sketchology.proto.ToolParams.ToolType');
+
+
+
+/**
+ * Holds the state of the ink brush. Toolbar widgets update BrushModel, which in
+ * turn dispatches CHANGE events to update the toolbar components' states.
+ * @constructor
+ * @extends {ink.Model}
+ * @param {!goog.events.EventTarget} root Unused
+ */
+ink.BrushModel = function(root) {
+  ink.BrushModel.base(this, 'constructor');
+  /**
+   * Last brush color (not including erase color).
+   * @private {string}
+   */
+  this.color_ = ink.BrushModel.DEFAULT_DRAW_COLOR;
+
+  /**
+   * The active stroke width of the brush (includes erase size).
+   * @private {number}
+   */
+  this.strokeWidth_ = ink.BrushModel.DEFAULT_DRAW_SIZE;
+
+  /**
+   * @private {boolean}
+   */
+  this.isErasing_ = ink.BrushModel.DEFAULT_ISERASING;
+
+  /**
+   * @private {sketchology.proto.ToolParams.ToolType}
+   */
+  this.toolType_ = sketchology.proto.ToolParams.ToolType.LINE;
+
+  /**
+   * @private {sketchology.proto.BrushType}
+   */
+  this.brushType_ = sketchology.proto.BrushType.CALLIGRAPHY;
+
+  /**
+   * @private {string}
+   */
+  this.shape_ = 'CALLIGRAPHY';
+};
+goog.inherits(ink.BrushModel, ink.Model);
+ink.Model.addGetter(ink.BrushModel);
+
+
+/**
+ * @const {string}
+ */
+ink.BrushModel.DEFAULT_DRAW_COLOR = '#000000';
+
+
+/**
+ * @const {number}
+ */
+ink.BrushModel.DEFAULT_DRAW_SIZE = 0.6;
+
+
+/**
+ * @const {string}
+ */
+ink.BrushModel.DEFAULT_ERASE_COLOR = '#FFFFFF';
+
+/**
+ * @const {boolean}
+ */
+ink.BrushModel.DEFAULT_ISERASING = false;
+
+
+/**
+ * The events fired by the BrushModel.
+ * @enum {string} The event types for the BrushModel.
+ */
+ink.BrushModel.EventType = {
+  /**
+   * Fired when the BrushModel is changed.
+   */
+  CHANGE: goog.events.getUniqueId('change')
+};
+
+
+/**
+ * @const {Object}
+ */
+ink.BrushModel.SHAPE_TO_TOOLTYPE = {
+  'AIRBRUSH': sketchology.proto.ToolParams.ToolType.LINE,
+  'CALLIGRAPHY': sketchology.proto.ToolParams.ToolType.LINE,
+  'EDIT': sketchology.proto.ToolParams.ToolType.EDIT,
+  'ERASER': sketchology.proto.ToolParams.ToolType.LINE,
+  'HIGHLIGHTER': sketchology.proto.ToolParams.ToolType.LINE,
+  'INKPEN': sketchology.proto.ToolParams.ToolType.LINE,
+  'MAGIC_ERASE': sketchology.proto.ToolParams.ToolType.MAGIC_ERASE,
+  'MARKER': sketchology.proto.ToolParams.ToolType.LINE,
+  'PENCIL': sketchology.proto.ToolParams.ToolType.LINE,
+  'BALLPOINT': sketchology.proto.ToolParams.ToolType.LINE,
+  'BALLPOINT_IN_PEN_MODE_ELSE_MARKER':
+      sketchology.proto.ToolParams.ToolType.LINE,
+  'QUERY': sketchology.proto.ToolParams.ToolType.QUERY,
+};
+
+
+/**
+ * @const {Object}
+ */
+ink.BrushModel.SHAPE_TO_BRUSHTYPE = {
+  'AIRBRUSH': sketchology.proto.BrushType.AIRBRUSH,
+  'CALLIGRAPHY': sketchology.proto.BrushType.CALLIGRAPHY,
+  'ERASER': sketchology.proto.BrushType.ERASER,
+  'HIGHLIGHTER': sketchology.proto.BrushType.HIGHLIGHTER,
+  'INKPEN': sketchology.proto.BrushType.INKPEN,
+  'MARKER': sketchology.proto.BrushType.MARKER,
+  'BALLPOINT': sketchology.proto.BrushType.BALLPOINT,
+  'BALLPOINT_IN_PEN_MODE_ELSE_MARKER':
+      sketchology.proto.BrushType.BALLPOINT_IN_PEN_MODE_ELSE_MARKER,
+  'PENCIL': sketchology.proto.BrushType.PENCIL,
+};
+
+
+/**
+ * @param {string} color The color in hex.
+ */
+ink.BrushModel.prototype.setColor = function(color) {
+  this.color_ = color;
+  this.dispatchEvent(ink.BrushModel.EventType.CHANGE);
+};
+
+
+/**
+ * @param {number} strokeWidth The brush's stroke width.
+ */
+ink.BrushModel.prototype.setStrokeWidth = function(strokeWidth) {
+  this.strokeWidth_ = strokeWidth;
+  this.dispatchEvent(ink.BrushModel.EventType.CHANGE);
+};
+
+
+/**
+ * @param {boolean} isErasing Whether user is erasing or not.
+ */
+ink.BrushModel.prototype.setIsErasing = function(isErasing) {
+  this.isErasing_ = isErasing;
+  this.dispatchEvent(ink.BrushModel.EventType.CHANGE);
+};
+
+
+/**
+ * @param {string} shape The brush shape, which is either a brush type or a tool
+ * type.  If it's a brush type, implies tool type LINE.
+ */
+ink.BrushModel.prototype.setShape = function(shape) {
+  this.toolType_ = ink.BrushModel.SHAPE_TO_TOOLTYPE[shape];
+  this.brushType_ = ink.BrushModel.SHAPE_TO_BRUSHTYPE[shape] !== undefined ?
+      ink.BrushModel.SHAPE_TO_BRUSHTYPE[shape] :
+      this.brushType_;
+  this.shape_ = shape;
+  this.dispatchEvent(ink.BrushModel.EventType.CHANGE);
+};
+
+
+/**
+ * @return {string} The last used shape.
+ */
+ink.BrushModel.prototype.getShape = function() {
+  return this.shape_;
+};
+
+
+/**
+ * @return {string} The last draw color in hex (excluding erase color).
+ */
+ink.BrushModel.prototype.getColor = function() {
+  return this.color_;
+};
+
+
+/**
+ * Gets the current color being drawn on the screen (including erase color).
+ * @return {string} The brush color in hex.
+ */
+ink.BrushModel.prototype.getActiveColor = function() {
+  if (!this.isErasing_) {
+    return this.color_;
+  } else {
+    return ink.BrushModel.DEFAULT_ERASE_COLOR;
+  }
+};
+
+
+/**
+ * Wraps getActiveColor() by returning the numeric rgb of the color.
+ * @return {number} The brush color in numeric rbg.
+ */
+ink.BrushModel.prototype.getActiveColorNumericRbg = function() {
+  return parseInt(this.getActiveColor().substring(1), 16);
+};
+
+
+/**
+ * @return {number} Percentage size for stroke width, [0, 1].
+ *
+ * See sengine.proto SizeType
+ */
+ink.BrushModel.prototype.getStrokeWidth = function() {
+  return this.strokeWidth_;
+};
+
+
+/**
+ * @return {boolean} Whether user is erasing.
+ */
+ink.BrushModel.prototype.getIsErasing = function() {
+  return this.isErasing_;
+};
+
+
+/**
+ * @return {sketchology.proto.BrushType} The brush type for line
+ * tool.
+ */
+ink.BrushModel.prototype.getBrushType = function() {
+  return this.brushType_;
+};
+
+
+/**
+ * @return {sketchology.proto.ToolParams.ToolType} The tool type.
+ */
+ink.BrushModel.prototype.getToolType = function() {
+  return this.toolType_;
+};
+
diff --git a/third_party/ink/sketchology/public/js/common/color.js b/third_party/ink/sketchology/public/js/common/color.js
new file mode 100644
index 0000000..1a61210
--- /dev/null
+++ b/third_party/ink/sketchology/public/js/common/color.js
@@ -0,0 +1,113 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+goog.provide('ink.Color');
+
+
+
+/**
+ * 32bit color representation. Each channels is a 8 bit uint.
+ * @param {number} argb The 32-bit color.
+ * @constructor
+ * @struct
+ */
+ink.Color = function(argb) {
+  /** @type {number} */
+  this.argb = argb;
+
+  /** @type {number} */
+  this.a = ink.Color.alphaFromArgb(argb);
+
+  /** @type {number} */
+  this.r = ink.Color.redFromArgb(argb);
+
+  /** @type {number} */
+  this.g = ink.Color.greenFromArgb(argb);
+
+  /** @type {number} */
+  this.b = ink.Color.blueFromArgb(argb);
+};
+
+
+/**
+ * @return {string} The color string (excluding alpha) that can be used as a
+ *     fillStyle.
+ */
+ink.Color.prototype.getRgbString = function() {
+  return 'rgb(' + [this.r, this.g, this.b].join(',') + ')';
+};
+
+
+/** @return {string} The color string that can be used as a fillStyle. */
+ink.Color.prototype.getRgbaString = function() {
+  return 'rgba(' + [this.r, this.g, this.b, this.a / 255].join(',') + ')';
+};
+
+
+/** @return {Uint32Array} color as rgba 32-bit unsigned integer */
+ink.Color.prototype.getRgbaUint32 = function() {
+  return new Uint32Array(
+      [(this.r << 24) | (this.g << 16) | (this.b << 8) | this.a]);
+};
+
+
+/**
+ * @return {number} The alpha in the range 0-1 that can be used as a
+ *     globalAlpha.
+ */
+ink.Color.prototype.getAlphaAsFloat = function() {
+  return this.a / 255;
+};
+
+
+/**
+ * Helper function that returns a function that right logical shifts by the
+ * provided amount and masks off the result.
+ * @param {number} shiftAmount The amount that the function should shift by.
+ * @return {!Function}
+ * @private
+ */
+ink.Color.shiftAndMask_ = function(shiftAmount) {
+  return function(argb) {
+    return (argb >>> shiftAmount) & 0xFF;
+  };
+};
+
+
+/**
+ * @param {number} argb The argb number.
+ * @return {number} alpha in the range 0 to 255.
+ */
+ink.Color.alphaFromArgb = ink.Color.shiftAndMask_(24);
+
+
+/**
+ * @param {number} argb The argb number.
+ * @return {number} red in the range 0 to 255.
+ */
+ink.Color.redFromArgb = ink.Color.shiftAndMask_(16);
+
+
+/**
+ * @param {number} argb The argb number.
+ * @return {number} green in the range 0 to 255.
+ */
+ink.Color.greenFromArgb = ink.Color.shiftAndMask_(8);
+
+
+/**
+ * @param {number} argb The argb number.
+ * @return {number} blue in the range 0 to 255.
+ */
+ink.Color.blueFromArgb = ink.Color.shiftAndMask_(0);
+
+
+/** @type {!ink.Color} */
+ink.Color.BLACK = new ink.Color(0xFF000000);
+
+
+/** @type {!ink.Color} */
+ink.Color.WHITE = new ink.Color(0xFFFFFFFF);
+
+/** @type {!ink.Color} */
+ink.Color.DEFAULT_BACKGROUND_COLOR = new ink.Color(0xFFFAFAFA);
diff --git a/third_party/ink/sketchology/public/js/common/element_listener.js b/third_party/ink/sketchology/public/js/common/element_listener.js
new file mode 100644
index 0000000..a149f25
--- /dev/null
+++ b/third_party/ink/sketchology/public/js/common/element_listener.js
@@ -0,0 +1,32 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+/**
+ * @fileoverview Element listener interface declaration
+ */
+goog.provide('ink.ElementListener');
+
+/**
+ * @interface
+ */
+ink.ElementListener = function() {};
+
+/**
+ * @param {string} uuid
+ * @param {string} encodedElement
+ * @param {string} encodedTransform
+ */
+ink.ElementListener.prototype.onElementCreated = function(
+    uuid, encodedElement, encodedTransform) {};
+
+/**
+ * @param {Array.<string>} uuids
+ * @param {Array.<string>} encodedTransforms
+ */
+ink.ElementListener.prototype.onElementsMutated = function(
+    uuids, encodedTransforms) {};
+
+/**
+ * @param {Array.<string>} uuids
+ */
+ink.ElementListener.prototype.onElementsRemoved = function(uuids) {};
diff --git a/third_party/ink/sketchology/public/js/common/model.js b/third_party/ink/sketchology/public/js/common/model.js
new file mode 100644
index 0000000..0a726a4
--- /dev/null
+++ b/third_party/ink/sketchology/public/js/common/model.js
@@ -0,0 +1,89 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+goog.provide('ink.Model');
+
+goog.require('goog.asserts');
+goog.require('goog.events.EventTarget');
+goog.require('ink.util');
+
+
+
+/**
+ * A generic model base class. Each models here is a singleton per EventTarget
+ * hierarchy tree.
+ *
+ * @extends {goog.events.EventTarget}
+ * @constructor
+ * @struct
+ */
+ink.Model = function() {
+  ink.Model.base(this, 'constructor');
+};
+goog.inherits(ink.Model, goog.events.EventTarget);
+
+
+/**
+ * The models are attached to the root parent EventTargets. To have the models
+ * be automatically gc properly the relevant models need to actually be
+ * properties of those EventTargets. This property is initialized to avoid
+ * collisions with other JavaScript on the same page, similarly to the property
+ * that goog.getUid uses.
+ * @private {string}
+ */
+ink.Model.MODEL_INSTANCES_PROPERTY_ = 'ink_model_instances_' + Math.random();
+
+
+/**
+ * Adds the getter to the model constructor, allowing for the simpler
+ * ink.BrushModel.getInstance(this); instead of
+ * ink.Model.get(ink.BrushModel, this);
+ * @param {!function(new:ink.Model, !goog.events.EventTarget)} modelCtor
+ *      The model constructor.
+ */
+ink.Model.addGetter = function(modelCtor) {
+  /**
+   * @param {!goog.events.EventTarget} observer
+   * @return {!ink.Model}
+   */
+  modelCtor.getInstance = function(observer) {
+    goog.asserts.assertObject(observer);
+    return ink.Model.get(modelCtor, observer);
+  };
+};
+
+
+/**
+ * Gets the relevant model for the provided viewer. The viewer should be a
+ * goog.ui.Component that has entered the document or a goog.events.EventTarget
+ * that has already had its parentEventTarget set.
+ *
+ * Note: This currently assumes that the provided models are singletons per
+ * EventTarget hierarchy tree. A more flexible design for deciding what level
+ * to have models should be added here if usage demands it.
+ *
+ * @param {!function(new:ink.Model, !goog.events.EventTarget)} modelCtor
+ * @param {!goog.events.EventTarget} observer
+ * @return {!ink.Model}
+ */
+ink.Model.get = function(modelCtor, observer) {
+  // TODO(esrauch): Maybe this should be implemented based on dom elements
+  // instead of the goog.ui.Component hierarchy. As it is, a stray setParent()
+  // call could cause the Model instance to suprisingly change for the same
+  // observer. On the other hand, reading the dom is slower and also can cause
+  // a brower reflow unnecessarily and this way also allows for vanilla
+  // EventTargets to get the relevant Models.
+  var root = ink.util.getRootParentComponent(observer);
+  var models = root[ink.Model.MODEL_INSTANCES_PROPERTY_];
+  if (!models) {
+    root[ink.Model.MODEL_INSTANCES_PROPERTY_] = models = {};
+  }
+  var key = goog.getUid(modelCtor);
+  var oldInstance = models[key];
+  if (oldInstance) {
+    return oldInstance;
+  }
+  var newInstance = new modelCtor(root);
+  models[key] = newInstance;
+  return newInstance;
+};
diff --git a/third_party/ink/sketchology/public/js/common/proto_serializer.js b/third_party/ink/sketchology/public/js/common/proto_serializer.js
new file mode 100644
index 0000000..dc99eeb
--- /dev/null
+++ b/third_party/ink/sketchology/public/js/common/proto_serializer.js
@@ -0,0 +1,77 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+goog.provide('ink.ProtoSerializer');
+
+
+goog.require('goog.crypt.base64');
+goog.require('goog.proto2.ObjectSerializer');  // for debugging
+goog.require('net.proto2.contrib.WireSerializer');
+
+
+
+/**
+ * A proto serializer / deserializer to and from base-64 encoded wire format.
+ * @constructor
+ * @struct
+ */
+ink.ProtoSerializer = function() {
+  /** @private {!net.proto2.contrib.WireSerializer} */
+  this.wireSerializer_ = new net.proto2.contrib.WireSerializer();
+};
+
+
+/**
+ * @param {!goog.proto2.Message} e proto to serialize
+ * @return {string} The serialized proto as a base 64 encoded string.
+ */
+ink.ProtoSerializer.prototype.serializeToBase64 = function(e) {
+  var buf = this.wireSerializer_.serialize(e);
+  return goog.crypt.base64.encodeByteArray(buf);
+};
+
+
+/**
+ * Deserializes the given opaque serialized object to a jspb object.
+ *
+ * @param {string} item serialized object as base64 text
+ * @param {!goog.proto2.Message} proto Proto to deserialize into
+ * @return {!goog.proto2.Message}
+ */
+ink.ProtoSerializer.prototype.safeDeserialize = function(item, proto) {
+  var buf = goog.crypt.base64.decodeStringToByteArray(item);
+  this.wireSerializer_.deserializeTo(proto, new Uint8Array(buf));
+  return proto;
+};
+
+
+/**
+ * @param {!sketchology.proto.Element} p
+ * @return {boolean} Whether the provided Element appears valid.
+ * @private
+ */
+ink.ProtoSerializer.prototype.isValid_ = function(p) {
+  if (p == null) {
+    return false;
+  }
+
+  if (!p.hasStroke()) {
+    return false;
+  }
+
+  return true;
+};
+
+
+/**
+ * Returns a human-readable representation of a proto.
+ *
+ * @param {!goog.proto2.Message} p The proto to debug.
+ * @return {string} A nice string to ponder.
+ * @private
+ */
+ink.ProtoSerializer.prototype.debugProto_ = function(p) {
+  var obj = new goog.proto2.ObjectSerializer(
+      goog.proto2.ObjectSerializer.KeyOption.NAME).serialize(p);
+  return JSON.stringify(obj, null, '  ');
+};
diff --git a/third_party/ink/sketchology/public/js/common/undo_state_change_event.js b/third_party/ink/sketchology/public/js/common/undo_state_change_event.js
new file mode 100644
index 0000000..5b65a277
--- /dev/null
+++ b/third_party/ink/sketchology/public/js/common/undo_state_change_event.js
@@ -0,0 +1,28 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+goog.provide('ink.UndoStateChangeEvent');
+
+goog.require('goog.events');
+goog.require('goog.events.Event');
+
+
+
+/**
+ * @param {boolean} canUndo Whether there is undo state available.
+ * @param {boolean} canRedo Whether there is redo state available.
+ * @constructor
+ * @struct
+ * @extends {goog.events.Event}
+ */
+ink.UndoStateChangeEvent = function(canUndo, canRedo) {
+  ink.UndoStateChangeEvent.base(
+      this, 'constructor',ink.UndoStateChangeEvent.EVENT_TYPE);
+  this.canUndo = canUndo;
+  this.canRedo = canRedo;
+};
+goog.inherits(ink.UndoStateChangeEvent, goog.events.Event);
+
+
+/** @type {string} */
+ink.UndoStateChangeEvent.EVENT_TYPE = goog.events.getUniqueId('undo-state');
diff --git a/third_party/ink/sketchology/public/js/common/util.js b/third_party/ink/sketchology/public/js/common/util.js
new file mode 100644
index 0000000..85fd56e
--- /dev/null
+++ b/third_party/ink/sketchology/public/js/common/util.js
@@ -0,0 +1,292 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+goog.provide('ink.util');
+
+goog.require('goog.dom');
+goog.require('goog.events');
+goog.require('goog.math.Size');
+goog.require('protos.research.ink.InkEvent');
+
+
+/** @enum {string} */
+ink.util.SEngineType = {
+  IN_MEMORY: 'makeSEngineInMemory',
+  LONGFORM: 'makeLongformSEngine',
+  PASSTHROUGH_DOCUMENT: 'makeSEnginePassthroughDocument'
+};
+
+
+/**
+ * @typedef {{
+ *   getModel: (function():ink.util.RealtimeModel)
+ * }}
+ */
+ink.util.RealtimeDocument;
+
+
+/**
+ * @typedef {{
+ *  getRoot: (function():ink.util.RealtimeRoot)
+ * }}
+ */
+ink.util.RealtimeModel;
+
+
+/**
+ * @typedef {{
+ *   get: (function(string):ink.util.RealtimePages)
+ * }}
+ */
+ink.util.RealtimeRoot;
+
+
+/**
+ * @typedef {{
+ *   get: (function(number):ink.util.RealtimePage),
+ *   xhigh: number,
+ *   xlow: number,
+ *   yhigh: number,
+ *   ylow: number
+ * }}
+ */
+ink.util.RealtimePages;
+
+
+/**
+ * @typedef {{
+ *   get: (function(string):ink.util.RealtimeElements)
+ * }}
+ */
+ink.util.RealtimePage;
+
+
+/**
+ * @typedef {{
+ *   asArray: (function():Array.<ink.util.RealtimeElement>)
+ * }}
+ */
+ink.util.RealtimeElements;
+
+
+/**
+ * @typedef {{
+ *   get: (function(string):string)
+ * }}
+ */
+ink.util.RealtimeElement;
+
+
+/**
+ * Wrapper used for event handlers to pass the event target instead of the
+ * event.
+ * @param {!Function} callback The event handler function.
+ * @param {Object=} opt_handler Element in whole scope to call the callback.
+ * @return {!Function} Wrapped function.
+ */
+ink.util.eventTargetWrapper = function(callback, opt_handler) {
+  return function(evt) {
+    var self = /** @type {Object} */ (this);
+    callback.call(opt_handler || self, evt.target);
+  };
+};
+
+
+/**
+ * @param {!goog.events.EventTarget} child The child to get the root parent
+ *     EventTarget for.
+ * @return {!goog.events.EventTarget} The root parent.
+ * TODO(esrauch): Rename this function and consider moving it to a new
+ * whiteboard/util.js file.
+ */
+ink.util.getRootParentComponent = function(child) {
+  var current = child;
+  var parent = current.getParentEventTarget();
+  while (parent) {
+    current = parent;
+    parent = current.getParentEventTarget();
+  }
+  return current;
+};
+
+
+/**
+ * Downloads an image, renders it to a canvas, returns the raw bytes.
+ * @param {string} imgSrc
+ * @param {Function} callback
+ */
+ink.util.getImageBytes = function(imgSrc, callback) {
+  var canvasElement = goog.dom.createElement(goog.dom.TagName.CANVAS);
+  var imgElement = goog.dom.createElement(goog.dom.TagName.IMG);
+  imgElement.setAttribute(
+      'style',
+      'position:absolute;visibility:hidden;top:-1000px;left:-1000px;');
+  imgElement.crossOrigin = 'Anonymous';
+
+  goog.events.listenOnce(imgElement, 'load', function() {
+    var width = imgElement.width;
+    var height = imgElement.height;
+    canvasElement.width = width;
+    canvasElement.height = height;
+    var ctx = canvasElement.getContext('2d');
+    ctx.drawImage(imgElement, 0, 0);
+    var data = ctx.getImageData(0, 0, width, height);
+
+    document.body.removeChild(imgElement);
+
+    callback(data.data, new goog.math.Size(width, height));
+  });
+
+  imgElement.setAttribute('src', imgSrc);
+  document.body.appendChild(imgElement);
+};
+
+
+/**
+ * Creates document events.
+ * @param {!protos.research.ink.InkEvent.Host} host
+ * @param {!protos.research.ink.InkEvent.DocumentEvent.DocumentEventType} type
+ * @return {!protos.research.ink.InkEvent}
+ */
+ink.util.createDocumentEvent = function(host, type) {
+  var eventProto = new protos.research.ink.InkEvent();
+  eventProto.setHost(host);
+  eventProto.setEventType(
+      protos.research.ink.InkEvent.EventType.DOCUMENT_EVENT);
+  var documentEvent = new protos.research.ink.InkEvent.DocumentEvent();
+  documentEvent.setEventType(type);
+  eventProto.setDocumentEvent(documentEvent);
+  return eventProto;
+};
+
+
+/**
+ * Helper for constructing a document created event
+ *
+ * @param {!protos.research.ink.InkEvent.Host} host
+ * @param {!protos.research.ink.InkEvent.DocumentEvent.DocumentState} state
+ * @param {number} firstLoadTime (which is when the first byte is loaded)
+ * @param {number} startLoadTime (which is when the document is first editable).
+ * @return {!protos.research.ink.InkEvent}
+ */
+ink.util.createDocumentOpenedEvent = function(
+    host, state, firstLoadTime, startLoadTime) {
+  var type =
+      protos.research.ink.InkEvent.DocumentEvent.DocumentEventType.OPENED;
+  var ev = ink.util.createDocumentEvent(host, type);
+  var openedEvent =
+      new protos.research.ink.InkEvent.DocumentEvent.OpenedEvent();
+  openedEvent.setMillisUntilFirstByteLoaded(firstLoadTime.toString());
+  openedEvent.setMillisUntilEditable((goog.now() - startLoadTime).toString());
+  var documentEvent = ev.getDocumentEventOrDefault();
+  documentEvent.setOpenedEvent(openedEvent);
+  documentEvent.setDocumentState(state);
+  return ev;
+};
+
+
+/**
+ * Helper for constructing a collaborator joined logging event.
+ *
+ * @param {!protos.research.ink.InkEvent.Host} host
+ * @param {!protos.research.ink.InkEvent.DocumentEvent.DocumentState} state
+ * @param {boolean} isMe
+ * @return {!protos.research.ink.InkEvent}
+ */
+ink.util.createCollaboratorJoinedDocumentEvent = function(host, state, isMe) {
+  var type = protos.research.ink.InkEvent.DocumentEvent.DocumentEventType
+                 .COLLABORATOR_JOINED;
+  var ev = ink.util.createDocumentEvent(host, type);
+  var collaboratorJoinedEvent =
+      new protos.research.ink.InkEvent.DocumentEvent.CollaboratorJoined();
+  collaboratorJoinedEvent.setIsMe(isMe);
+  var documentEvent = ev.getDocumentEventOrDefault();
+  documentEvent.setCollaboratorJoinedEvent(collaboratorJoinedEvent);
+  documentEvent.setDocumentState(state);
+  return ev;
+};
+
+
+/**
+ * @const
+ */
+ink.util.SHAPE_TO_LOG_TOOLTYPE = {
+  'CALLIGRAPHY': protos.research.ink.InkEvent.ToolbarEvent.ToolType.CALLIGRAPHY,
+  'EDIT': protos.research.ink.InkEvent.ToolbarEvent.ToolType.EDIT_TOOL,
+  'HIGHLIGHTER': protos.research.ink.InkEvent.ToolbarEvent.ToolType.HIGHLIGHTER,
+  'MAGIC_ERASE': protos.research.ink.InkEvent.ToolbarEvent.ToolType.MAGIC_ERASER,
+  'MARKER': protos.research.ink.InkEvent.ToolbarEvent.ToolType.MARKER
+};
+
+
+/**
+ * Creates toolbar events.
+ * @param {protos.research.ink.InkEvent.Host} host
+ * @param {protos.research.ink.InkEvent.ToolbarEvent.ToolEventType} type
+ * @param {string} toolType
+ * @param {string} color
+ * @return {protos.research.ink.InkEvent}
+ */
+ink.util.createToolbarEvent = function(host, type, toolType, color) {
+  var eventProto = new protos.research.ink.InkEvent();
+  eventProto.setHost(host);
+  eventProto.setEventType(protos.research.ink.InkEvent.EventType.TOOLBAR_EVENT);
+  var toolbarEvent = new protos.research.ink.InkEvent.ToolbarEvent();
+  // Alpha is always 0xFF. This does #RRGGBB -> #AARRGGBB -> 0xAARRGGBB.
+  var colorAsNumber = parseInt('ff' + color.substring(1, 7), 16);
+  toolbarEvent.setColor(colorAsNumber);
+  toolbarEvent.setToolType(
+      ink.util.SHAPE_TO_LOG_TOOLTYPE[toolType] ||
+      protos.research.ink.InkEvent.ToolbarEvent.ToolType.UNKNOWN_TOOL_TYPE);
+  toolbarEvent.setToolEventType(type);
+  eventProto.setToolbarEvent(toolbarEvent);
+  return eventProto;
+};
+
+
+// Note: These helpers are included here because we do not use the closure
+// browser event wrappers to avoid additional GC pauses.  Logic forked from
+// cs/piper///depot/google3/javascript/closure/events/browserevent.js?l=337
+
+/**
+ * "Action button" is  MouseEvent.button equal to 0 (main button) and no
+ * control-key for Mac right-click action.  The button property is the one
+ * that was responsible for triggering a mousedown or mouseup event.
+ *
+ * @param {MouseEvent} evt
+ * @return {boolean}
+ */
+ink.util.isMouseActionButton = function(evt) {
+  return evt.button == 0 &&
+         !(goog.userAgent.WEBKIT && goog.userAgent.MAC && evt.ctrlKey);
+};
+
+
+/**
+ * MouseEvent.buttons has 1 (main button) held down, and no control-key
+ * for Mac right-click drag.  This is for which button is currently held down,
+ * e.g. during a mousemove event.
+ *
+ * @param {MouseEvent} evt
+ * @return {boolean}
+ */
+ink.util.hasMouseActionButton = function(evt) {
+  return (evt.buttons & 1) == 1 &&
+         !(goog.userAgent.WEBKIT && goog.userAgent.MAC && evt.ctrlKey);
+};
+
+
+/**
+ * Checks MouseEvent.buttons has 2 (secondary button) held down, or 1 (main
+ * button) with control key for Mac right-click drag.  This is for which
+ * button is currently held down, e.g. during a mousemove event.
+ *
+ * @param {MouseEvent} evt
+ * @return {boolean}
+ */
+ink.util.hasMouseSecondaryButton = function(evt) {
+  return (evt.buttons & 2) == 2 ||
+         ((evt.buttons & 1) == 1 && goog.userAgent.WEBKIT &&
+          goog.userAgent.MAC && evt.ctrlKey);
+};
+
diff --git a/third_party/ink/sketchology/public/nacl/embed.soy.js b/third_party/ink/sketchology/public/nacl/embed.soy.js
new file mode 100644
index 0000000..e0b4325
--- /dev/null
+++ b/third_party/ink/sketchology/public/nacl/embed.soy.js
@@ -0,0 +1,50 @@
+// Copyright 2017 The Chromium Authors. 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 was automatically generated from embed.soy.
+// Please don't edit this file by hand.
+
+/**
+ * @fileoverview Templates in namespace ink.soy.nacl.
+ * @public
+ */
+
+goog.provide('ink.soy.nacl.canvasHTML');
+
+goog.require('goog.soy.data.SanitizedContent');
+goog.require('soy');
+goog.require('soy.asserts');
+goog.require('soydata.VERY_UNSAFE');
+
+
+/**
+ * @param {ink.soy.nacl.canvasHTML.Params} opt_data
+ * @param {Object<string, *>=} opt_ijData
+ * @param {Object<string, *>=} opt_ijData_deprecated
+ * @return {!goog.soy.data.SanitizedHtml}
+ * @suppress {checkTypes}
+ */
+ink.soy.nacl.canvasHTML = function(opt_data, opt_ijData, opt_ijData_deprecated) {
+  opt_ijData = opt_ijData_deprecated || opt_ijData;
+  /** @type {!goog.soy.data.SanitizedContent|string} */
+  var manifestUrl = soy.asserts.assertType(goog.isString(opt_data.manifestUrl) || opt_data.manifestUrl instanceof goog.soy.data.SanitizedContent, 'manifestUrl', opt_data.manifestUrl, '!goog.soy.data.SanitizedContent|string');
+  /** @type {!goog.soy.data.SanitizedContent|string} */
+  var useMSAA = soy.asserts.assertType(goog.isString(opt_data.useMSAA) || opt_data.useMSAA instanceof goog.soy.data.SanitizedContent, 'useMSAA', opt_data.useMSAA, '!goog.soy.data.SanitizedContent|string');
+  /** @type {!goog.soy.data.SanitizedContent|string} */
+  var useSingleBuffer = soy.asserts.assertType(goog.isString(opt_data.useSingleBuffer) || opt_data.useSingleBuffer instanceof goog.soy.data.SanitizedContent, 'useSingleBuffer', opt_data.useSingleBuffer, '!goog.soy.data.SanitizedContent|string');
+  /** @type {!goog.soy.data.SanitizedContent|string} */
+  var sengineType = soy.asserts.assertType(goog.isString(opt_data.sengineType) || opt_data.sengineType instanceof goog.soy.data.SanitizedContent, 'sengineType', opt_data.sengineType, '!goog.soy.data.SanitizedContent|string');
+  return soydata.VERY_UNSAFE.ordainSanitizedHtml(((goog.DEBUG && soy.$$debugSoyTemplateInfo) ? '<!--dta_of(ink.soy.nacl.canvasHTML, third_party/sketchology/public/nacl/embed.soy, 3)-->' : '') + '<style' + (opt_ijData && opt_ijData.csp_nonce ? ' nonce="' + soy.$$escapeHtmlAttribute(opt_ijData && opt_ijData.csp_nonce) + '"' : '') + '>\n    #ink-engine-hwoverlay {\n      display: none;\n      position: absolute;\n      width: 5px;\n      height: 5px;\n      left: 0px;\n      top: 0px;\n      /* Transforms and semi-transparent color are used to ensure the div\n       * prevents use of a hardware overlay for the underlying canvas element,\n       * despite future optimizations to the hardware overlay eligibility\n       * detection in ChromeOS.  See b/64569245 for details */\n      background-color: rgba(0, 0, 0, 0.01);\n      transform: translate3d(0.33, 0.14, 0);\n    }\n  </style><embed id="ink-engine" use_msaa="' + soy.$$escapeHtmlAttribute(useMSAA) + '" use_single_buffer="' + soy.$$escapeHtmlAttribute(useSingleBuffer) + '" src="' + soy.$$escapeHtmlAttribute(soy.$$filterNormalizeUri(manifestUrl)) + '" type="application/x-nacl" sengine_type="' + soy.$$escapeHtmlAttribute(sengineType) + '"><div id="ink-engine-hwoverlay"></div>' + ((goog.DEBUG && soy.$$debugSoyTemplateInfo) ? '<!--dta_cf(ink.soy.nacl.canvasHTML)-->' : ''));
+};
+/**
+ * @typedef {{
+ *  manifestUrl: (!goog.soy.data.SanitizedContent|string),
+ *  useMSAA: (!goog.soy.data.SanitizedContent|string),
+ *  useSingleBuffer: (!goog.soy.data.SanitizedContent|string),
+ *  sengineType: (!goog.soy.data.SanitizedContent|string),
+ * }}
+ */
+ink.soy.nacl.canvasHTML.Params;
+if (goog.DEBUG) {
+  ink.soy.nacl.canvasHTML.soyTemplateName = 'ink.soy.nacl.canvasHTML';
+}
diff --git a/third_party/ink/sketchology/public/nacl/sketchology_engine_wrapper.js b/third_party/ink/sketchology/public/nacl/sketchology_engine_wrapper.js
new file mode 100644
index 0000000..8ef2088
--- /dev/null
+++ b/third_party/ink/sketchology/public/nacl/sketchology_engine_wrapper.js
@@ -0,0 +1,764 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+/**
+ * @fileoverview Wrapper to call the Sketchology engine.
+ */
+
+goog.provide('ink.SketchologyEngineWrapper');
+
+goog.require('goog.asserts');
+goog.require('goog.events');
+goog.require('goog.labs.userAgent.platform');
+goog.require('goog.math.Box');
+goog.require('goog.math.Coordinate');
+goog.require('goog.math.Rect');
+goog.require('goog.math.Size');
+goog.require('goog.soy');
+goog.require('goog.ui.Component');
+goog.require('ink.Color');
+goog.require('ink.ElementListener');
+goog.require('ink.UndoStateChangeEvent');
+goog.require('ink.soy.nacl.canvasHTML');
+goog.require('ink.util');
+goog.require('net.proto2.contrib.WireSerializer');
+goog.require('sketchology.proto.BackgroundColor');
+goog.require('sketchology.proto.BackgroundImageInfo');
+goog.require('sketchology.proto.Command');
+goog.require('sketchology.proto.Flag');
+goog.require('sketchology.proto.FlagAssignment');
+goog.require('sketchology.proto.ImageInfo');
+goog.require('sketchology.proto.MutationPacket');
+goog.require('sketchology.proto.NoArgCommand');
+goog.require('sketchology.proto.OutOfBoundsColor');
+goog.require('sketchology.proto.PageProperties');
+goog.require('sketchology.proto.Rect');
+goog.require('sketchology.proto.SequencePoint');
+goog.require('sketchology.proto.SetCallbackFlags');
+goog.require('sketchology.proto.Snapshot');
+goog.require('sketchology.proto.ToolParams');
+
+
+
+/**
+ * @param {?string} engineUrl URL to the native client manifest file
+ * @param {ink.ElementListener} elementListener
+ * @param {function(number, number, Uint8ClampedArray)} onImageExportComplete
+ * @param {!ink.util.SEngineType} sengineType
+ * @struct
+ * @constructor
+ * @extends {goog.ui.Component}
+ */
+ink.SketchologyEngineWrapper = function(
+    engineUrl, elementListener, onImageExportComplete, sengineType) {
+  ink.SketchologyEngineWrapper.base(this, 'constructor');
+
+  goog.asserts.assert(engineUrl);
+  /** @private {string} */
+  this.engineUrl_ = engineUrl;
+
+  /** @private {ink.ElementListener} */
+  this.elementListener_ = elementListener;
+
+  /** @private {function(number, number, Uint8ClampedArray)} */
+  this.onImageExportComplete_ = onImageExportComplete;
+
+  // default document bounds
+  this.pageLeft_ = 0;
+  this.pageTop_ = 600;
+  this.pageRight_ = 800;
+  this.pageBottom_ = 0;
+
+  /**
+   * Protocol Buffer wire format serializer.
+   * @type {!net.proto2.contrib.WireSerializer}
+   * @private
+   */
+  this.wireSerializer_ = new net.proto2.contrib.WireSerializer();
+
+  /** @private */
+  this.lastBrushUpdate_ = goog.nullFunction;
+
+  /** @private */
+  this.penModeEnabled_ = false;
+
+  /** @private {boolean} */
+  this.listenersAdded_ = false;
+
+  /** @private {string} */
+  this.sengineType_ = /** @type {string} */ (sengineType);
+
+  /** @private {Array.<function(!sketchology.proto.Snapshot)>} */
+  this.snapshotCallbacks_ = [];
+
+  /** @private {Array.<function(!sketchology.proto.Snapshot)>} */
+  this.brixConversionCallbacks_ = [];
+
+  /** @private {Array.<!ink.util.RealtimeDocument>} */
+  this.brixDocuments_ = [];
+
+  /** @private {Array.<function(boolean)>} */
+  this.snapshotHasPendingMutationsCallbacks_ = [];
+
+  /** @private {Array.<function(sketchology.proto.MutationPacket)>} */
+  this.extractMutationPacketCallbacks_ = [];
+
+  /** @private {Array.<function(sketchology.proto.Snapshot)>} */
+  this.clearPendingMutationsCallbacks_ = [];
+
+  /** @private {Element} */
+  this.engineElement_;
+
+  /** @private {Object<number, !Function>} */
+  this.sequencePointCallbacks_ = {};
+
+  /** @private {number} */
+  this.nextSequencePointId_ = 0;
+};
+goog.inherits(ink.SketchologyEngineWrapper, goog.ui.Component);
+
+/** @override */
+ink.SketchologyEngineWrapper.prototype.createDom = function() {
+  const useMSAA = !goog.labs.userAgent.platform.isMacintosh() ||
+      // MSAA is disabled for MacOS 10.12.4 and prior: b/38280481
+      goog.labs.userAgent.platform.isVersionOrHigher('10.12.5');
+  const useSingleBuffer = goog.labs.userAgent.platform.isChromeOS();
+  const elem = goog.soy.renderAsElement(ink.soy.nacl.canvasHTML, {
+    manifestUrl: this.engineUrl_,
+    useMSAA: !!useMSAA + '',
+    useSingleBuffer: !!useSingleBuffer + '',
+    sengineType: this.sengineType_
+  });
+  this.setElementInternal(elem);
+  this.engineElement_ = elem.querySelector('#ink-engine');
+};
+
+/** @override */
+ink.SketchologyEngineWrapper.prototype.enterDocument = function() {
+  this.engineElement_.addEventListener(goog.events.EventType.LOAD, () => {
+    this.initGl();
+    this.lastBrushUpdate_();
+    this.assignFlag(
+        sketchology.proto.Flag.ENABLE_PEN_MODE, this.penModeEnabled_);
+  });
+};
+
+
+/** @enum {string} */
+ink.SketchologyEngineWrapper.EventType = {
+  CANVAS_INITIALIZED: goog.events.getUniqueId('gl_canvas_initialized'),
+  CANVAS_FATAL_ERROR: goog.events.getUniqueId('fatal_error'),
+  PEN_MODE_ENABLED: goog.events.getUniqueId('pen_mode_enabled')
+};
+
+
+/**
+ * An event fired when pen mode is enabled or disabled.
+ *
+ * @param {boolean} enabled
+ *
+ * @extends {goog.events.Event}
+ * @constructor
+ * @struct
+ */
+ink.SketchologyEngineWrapper.PenModeEnabled = function(enabled) {
+  ink.SketchologyEngineWrapper.PenModeEnabled.base(this, 'constructor',
+      ink.SketchologyEngineWrapper.EventType.PEN_MODE_ENABLED);
+
+  /** @type {boolean} */
+  this.enabled = enabled;
+};
+goog.inherits(ink.SketchologyEngineWrapper.PenModeEnabled, goog.events.Event);
+
+
+/** Poke the engine to wake up and start drawing */
+ink.SketchologyEngineWrapper.prototype.poke = function() {
+  this.engineElement_.postMessage(['poke', '']);
+};
+
+/**
+ * Global exit function for emscripten to call.
+ * @export
+ */
+ink.SketchologyEngineWrapper.exit = function() {
+  console.log('Engine requested exit.');
+};
+
+/**
+ * @param {sketchology.proto.Command} command
+ */
+ink.SketchologyEngineWrapper.prototype.handleCommand = function(command) {
+  var commandBytes = this.wireSerializer_.serialize(command);
+  var buf = new Uint8Array(commandBytes);
+  this.engineElement_.postMessage(['handleCommand', buf.buffer]);
+};
+
+/**
+ * Tells the engine to handle a message received remotely.
+ *
+ * @param {!Object<string, string>} bundle
+ */
+ink.SketchologyEngineWrapper.prototype.addElement = function(bundle) {
+  goog.asserts.assert(bundle);
+  this.engineElement_.postMessage(['addElementToEngine', {'bundle': bundle}]);
+};
+
+
+/**
+ * Add an encoded element bundle to the engine.
+ *
+ * @param {!Object<string, string>} bundle
+ * @param {string} belowUUID
+ */
+ink.SketchologyEngineWrapper.prototype.addElementBelow = function(
+    bundle, belowUUID) {
+  goog.asserts.assert(bundle);
+  this.engineElement_.postMessage(
+      ['addElementToEngineBelow', {'bundle': bundle, 'below_uuid': belowUUID}]);
+};
+
+
+/**
+ * @param {string} uuid
+ */
+ink.SketchologyEngineWrapper.prototype.removeElement = function(uuid) {
+  this.engineElement_.postMessage(['removeElement', uuid]);
+};
+
+
+/**
+ * NaCl does its own GL initialization, so we just hook up listeners.
+ */
+ink.SketchologyEngineWrapper.prototype.initGl = function() {
+  var elem = this.engineElement_;
+  this.setPageBounds(
+      this.pageLeft_, this.pageTop_, this.pageRight_, this.pageBottom_);
+  if (!this.listenersAdded_) {
+    this.listenersAdded_ = true;
+    elem.addEventListener('message', goog.bind(function(msg) {
+      if (!('event_type' in msg['data'])) {
+        return;  // Unknown event type!
+      }
+      var data = msg['data'];
+      switch (data['event_type']) {
+        case 'exit':
+          ink.SketchologyEngineWrapper.exit();
+          break;
+        case 'debug':
+          if (goog.DEBUG) {
+            console.log(data['message']);
+          }
+          break;
+        case 'image_export':
+          this.onImageExportComplete_(
+              data['width'], data['height'],
+              new Uint8ClampedArray(data['bytes']));
+          break;
+        case 'element_added':
+          if (this.elementListener_) {
+            this.elementListener_.onElementCreated(data['uuid'],
+                data['encoded_element'], data['encoded_transform']);
+          }
+          break;
+        case 'elements_mutated':
+          if (this.elementListener_) {
+            this.elementListener_.onElementsMutated(data['uuids'],
+                data['encoded_transforms']);
+          }
+          break;
+        case 'elements_removed':
+          if (this.elementListener_) {
+            this.elementListener_.onElementsRemoved(data['uuids']);
+          }
+          break;
+        case 'flag_changed':
+          if (data['which'] == sketchology.proto.Flag.ENABLE_PEN_MODE) {
+            this.penModeEnabled_ = data['enabled'];
+            this.dispatchEvent(
+                new ink.SketchologyEngineWrapper.PenModeEnabled(
+                    data['enabled']));
+          }
+          break;
+        case 'undo_redo_state_changed':
+          this.dispatchEvent(new ink.UndoStateChangeEvent(
+              !!data['can_undo'], !!data['can_redo']));
+          break;
+        case 'snapshot_gotten':
+          var proto = new sketchology.proto.Snapshot();
+          this.wireSerializer_.deserializeTo(proto, data['snapshot']);
+          this.snapshotCallbacks_.shift().call(null, proto);
+          break;
+        case 'brix_elements_converted':
+          var proto = new sketchology.proto.Snapshot();
+          this.wireSerializer_.deserializeTo(proto, data['snapshot']);
+          var pageProperties = new sketchology.proto.PageProperties();
+          var rect = new sketchology.proto.Rect();
+          var brixDoc = this.brixDocuments_.shift();
+          var model = brixDoc.getModel();
+          var root = model.getRoot();
+          var brixBounds = root.get('bounds');
+          rect.setXhigh(brixBounds.xhigh || 0);
+          rect.setXlow(brixBounds.xlow || 0);
+          rect.setYhigh(brixBounds.yhigh || 0);
+          rect.setYlow(brixBounds.ylow || 0);
+          pageProperties.setBounds(rect);
+          proto.setPageProperties(pageProperties);
+          this.brixConversionCallbacks_.shift().call(null, proto);
+          break;
+        case 'snapshot_has_pending_mutations':
+          this.snapshotHasPendingMutationsCallbacks_.shift().call(
+              null, data['has_mutations']);
+          break;
+        case 'extracted_mutation_packet':
+          var proto = new sketchology.proto.MutationPacket();
+          this.wireSerializer_.deserializeTo(proto, data['extraction_packet']);
+          this.extractMutationPacketCallbacks_.shift().call(null, proto);
+          break;
+        case 'cleared_pending_mutations':
+          var proto = new sketchology.proto.Snapshot();
+          this.wireSerializer_.deserializeTo(proto, data['snapshot']);
+          this.clearPendingMutationsCallbacks_.shift().call(null, proto);
+          break;
+        case 'hwoverlay':
+          this.setHardwareOverlay(!!data['enable']);
+          break;
+        case 'single_buffer':
+          // If the Native Client module was able to obtain a single buffered
+          // graphics context, flip the embed element to allow promotion to
+          // hardware overlay on Eve in landscape mode.
+          // TODO(b/64569245): Add support for all device orientations
+          this.engineElement_.style.transform = 'scaleY(-1)';
+          break;
+        case 'sequence_point_reached':
+          var id = data['id'];
+          var cb = this.sequencePointCallbacks_[id];
+          delete this.sequencePointCallbacks_[id];
+          cb();
+          break;
+      }
+    }, this));
+  }
+  this.dispatchEvent(ink.SketchologyEngineWrapper.EventType.CANVAS_INITIALIZED);
+};
+
+
+/**
+ * Sets the border image.
+ * @param {Uint8ClampedArray} data
+ * @param {goog.math.Size} size
+ * @param {string} uri
+ * @param {!sketchology.proto.Border} borderImageProto
+ * @param {number} outOfBoundsColor The out of bounds color in rgba 8888.
+ */
+ink.SketchologyEngineWrapper.prototype.setBorderImage = function(
+    data, size, uri, borderImageProto, outOfBoundsColor) {
+  var outOfBoundsColorProto = new sketchology.proto.OutOfBoundsColor();
+  outOfBoundsColorProto.setRgba(outOfBoundsColor);
+  var commandProto = new sketchology.proto.Command();
+  commandProto.setSetOutOfBoundsColor(outOfBoundsColorProto);
+  this.handleCommand(commandProto);
+
+  var msg = {
+    'imageData': data.buffer,
+    'uri': uri,
+    'width': size.width,
+    'height': size.height,
+    'assetType': sketchology.proto.ImageInfo.AssetType.BORDER
+  };
+  this.engineElement_.postMessage(['addImageData', msg]);
+
+  commandProto = new sketchology.proto.Command();
+  commandProto.setSetPageBorder(borderImageProto);
+  this.handleCommand(commandProto);
+};
+
+
+/**
+ * Sets the background image from a data URI.
+ *
+ * @param {Uint8ClampedArray} data
+ * @param {goog.math.Size} size
+ * @param {string} uri
+ * @param {!sketchology.proto.BackgroundImageInfo} bgImageProto
+ */
+ink.SketchologyEngineWrapper.prototype.setBackgroundImage = function(
+    data, size, uri, bgImageProto) {
+  var msg = {
+    'imageData': data.buffer,
+    'uri': uri,
+    'width': size.width,
+    'height': size.height,
+    'assetType': sketchology.proto.ImageInfo.AssetType.DEFAULT
+  };
+  this.engineElement_.postMessage(['addImageData', msg]);
+
+  var commandProto = new sketchology.proto.Command();
+  commandProto.setBackgroundImage(bgImageProto);
+  this.handleCommand(commandProto);
+};
+
+
+/**
+ * Set the background color
+ * @param {ink.Color} color
+ */
+ink.SketchologyEngineWrapper.prototype.setBackgroundColor = function(color) {
+  var bgColorProto = new sketchology.proto.BackgroundColor();
+  bgColorProto.setRgba(color.getRgbaUint32()[0]);
+  var commandProto = new sketchology.proto.Command();
+  commandProto.setBackgroundColor(bgColorProto);
+  this.handleCommand(commandProto);
+};
+
+
+/**
+ * Sets the camera position.
+ *
+ * @param {!goog.math.Rect} cameraRect The camera rect.
+ */
+ink.SketchologyEngineWrapper.prototype.setCamera = function(cameraRect) {
+  var camera = cameraRect.toBox();
+  var rectProto = new sketchology.proto.Rect();
+  // Top and bottom are reversed in Sketchology for "reasons."
+  rectProto.setYhigh(camera.bottom);
+  rectProto.setXhigh(camera.right);
+  rectProto.setYlow(camera.top);
+  rectProto.setXlow(camera.left);
+  var commandProto = new sketchology.proto.Command();
+  commandProto.setCameraPosition(rectProto);
+  this.handleCommand(commandProto);
+};
+
+
+/**
+ * @private
+ * @param {sketchology.proto.Rect} rectProto rect js proto with top/bottom
+ * reversed
+ * @return {goog.math.Rect} proper CSS rect with top/bottom correct
+ */
+ink.SketchologyEngineWrapper.prototype.convertRect_ = function(rectProto) {
+  // Top and bottom are reversed in Sketchology for "reasons."
+  var box = new goog.math.Box(
+      rectProto.getYlowOrDefault(), rectProto.getXhighOrDefault(),
+      rectProto.getYhighOrDefault(), rectProto.getXlowOrDefault());
+  return box ?
+      goog.math.Rect.createFromBox(box) :
+      null;
+};
+
+
+/**
+ * Create a scaled rectangle with a given size/center scaled by factor.
+ *
+ * @param {!goog.math.Coordinate} center
+ * @param {!goog.math.Size} size
+ * @param {number} factor The scale factor
+ *
+ * @return {goog.math.Rect} The scaled rectangle.
+ * @private
+ */
+ink.SketchologyEngineWrapper.prototype.getScaledRect_ = function(
+    center, size, factor) {
+  size.width /= factor;
+  size.height /= factor;
+
+  var x = center.x - size.width / 2;
+  var y = center.y - size.height / 2;
+
+  var cameraRect = new goog.math.Rect(x, y, size.width, size.height);
+
+  goog.asserts.assert(
+      Math.round(cameraRect.getCenter().x) === Math.round(center.x) &&
+      Math.round(cameraRect.getCenter().y) === Math.round(center.y));
+
+  return cameraRect;
+};
+
+
+/**
+ * Sets the brush parameters.
+ *
+ * @param {Uint32Array} color rgba 32-bit unsigned color
+ * @param {number} strokeWidth brush size percent [0,1]
+ * @param {sketchology.proto.ToolParams.ToolType} toolType
+ * @param {sketchology.proto.BrushType} brushType
+ */
+ink.SketchologyEngineWrapper.prototype.brushUpdate =
+    function(color, strokeWidth, toolType, brushType) {
+  var self = this;
+  this.lastBrushUpdate_ = function() {
+    // LINE tools need special handling
+    if (toolType != sketchology.proto.ToolParams.ToolType.LINE) {
+      var toolParamsProto = new sketchology.proto.ToolParams();
+      toolParamsProto.setTool(toolType);
+      var commandProto = new sketchology.proto.Command();
+      commandProto.setToolParams(toolParamsProto);
+      this.handleCommand(commandProto);
+    } else {
+      var updateBrushData = {
+        'brush': brushType,
+        'rgba': color[0],
+        'stroke_width': strokeWidth
+      };
+      self.engineElement_.postMessage(['updateBrush', updateBrushData]);
+    }
+  };
+  this.lastBrushUpdate_();
+};
+
+
+/** Clears the canvas. */
+ink.SketchologyEngineWrapper.prototype.clear = function() {
+  this.engineElement_.postMessage(['clear', '']);
+};
+
+
+/** Removes all elements from the document. */
+ink.SketchologyEngineWrapper.prototype.removeAll = function() {
+  this.engineElement_.postMessage(['removeAll', '']);
+};
+
+
+/**
+ * Sets or unsets readOnly on the canvas.
+ * @param {boolean} readOnly
+ */
+ink.SketchologyEngineWrapper.prototype.setReadOnly = function(readOnly) {
+  this.assignFlag(sketchology.proto.Flag.READ_ONLY_MODE, !!readOnly);
+};
+
+
+/**
+ * Assign a flag on the canvas
+ * @param {sketchology.proto.Flag} flag
+ * @param {boolean} enable
+ */
+ink.SketchologyEngineWrapper.prototype.assignFlag = function(flag, enable) {
+  var flagProto = new sketchology.proto.FlagAssignment();
+  flagProto.setFlag(flag);
+  flagProto.setBoolValue(!!enable);
+  var commandProto = new sketchology.proto.Command();
+  commandProto.setFlagAssignment(flagProto);
+  this.handleCommand(commandProto);
+};
+
+
+
+
+/**
+ * Sets element transforms.
+ * @param {Array.<string>} uuids
+ * @param {Array.<string>} encodedTransforms
+ */
+ink.SketchologyEngineWrapper.prototype.setElementTransforms = function(
+    uuids, encodedTransforms) {
+  if (uuids.length !== encodedTransforms.length) {
+    throw new Error('mismatch in transform array lengths');
+  }
+  this.engineElement_.postMessage([
+    'setElementTransforms',
+    {'uuids': uuids, 'encoded_transforms': encodedTransforms}
+  ]);
+};
+
+/**
+ * Set callback flags for what data is attached to element callbacks.
+ * @param {!sketchology.proto.SetCallbackFlags} setCallbackFlags
+ */
+ink.SketchologyEngineWrapper.prototype.setCallbackFlags = function(
+    setCallbackFlags) {
+  var commandProto = new sketchology.proto.Command();
+  commandProto.setSetCallbackFlags(setCallbackFlags);
+  this.handleCommand(commandProto);
+};
+
+
+/**
+ * Sets the size of the page.
+ * @param {number} left
+ * @param {number} top
+ * @param {number} right
+ * @param {number} bottom
+ */
+ink.SketchologyEngineWrapper.prototype.setPageBounds =
+    function(left, top, right, bottom) {
+  this.pageLeft_ = left;
+  this.pageTop_ = top;
+  this.pageRight_ = right;
+  this.pageBottom_ = bottom;
+  var pageBounds = new sketchology.proto.Rect();
+  pageBounds.setXlow(this.pageLeft_);
+  pageBounds.setYlow(this.pageBottom_);
+  pageBounds.setXhigh(this.pageRight_);
+  pageBounds.setYhigh(this.pageTop_);
+  var commandProto = new sketchology.proto.Command();
+  commandProto.setPageBounds(pageBounds);
+  this.handleCommand(commandProto);
+};
+
+
+/**
+ * Deselects anything selected with the edit tool.
+ */
+ink.SketchologyEngineWrapper.prototype.deselectAll = function() {
+  throw new Error('deselectAll not yet implemented for NaCl.');
+};
+
+
+/**
+ * Start the PNG export process.
+ *
+ * @param {!sketchology.proto.ImageExport} exportProto
+ */
+ink.SketchologyEngineWrapper.prototype.exportPng = function(exportProto) {
+  var commandProto = new sketchology.proto.Command();
+  commandProto.setImageExport(exportProto);
+  this.handleCommand(commandProto);
+};
+
+
+/**
+ * Simple undo.
+ */
+ink.SketchologyEngineWrapper.prototype.undo = function() {
+  var commandProto = new sketchology.proto.Command();
+  commandProto.setUndo(new sketchology.proto.NoArgCommand());
+  this.handleCommand(commandProto);
+};
+
+
+/**
+ * Simple redo.
+ */
+ink.SketchologyEngineWrapper.prototype.redo = function() {
+  var commandProto = new sketchology.proto.Command();
+  commandProto.setRedo(new sketchology.proto.NoArgCommand());
+  this.handleCommand(commandProto);
+};
+
+
+/**
+ * Returns the current snapshot.
+ * @param {function(!sketchology.proto.Snapshot)} callback
+ */
+ink.SketchologyEngineWrapper.prototype.getSnapshot = function(callback) {
+  this.snapshotCallbacks_.push(callback);
+  this.engineElement_.postMessage(['getSnapshot']);
+};
+
+
+/**
+ * Loads a document from a snapshot.
+ *
+ * @param {!sketchology.proto.Snapshot} snapshotProto
+ */
+ink.SketchologyEngineWrapper.prototype.loadFromSnapshot =
+    function(snapshotProto) {
+  var bytes = this.wireSerializer_.serialize(snapshotProto);
+  var buf = new Uint8Array(bytes);
+  this.engineElement_.postMessage(['loadFromSnapshot', buf.buffer]);
+};
+
+
+/**
+ * Gets the raw engine object. Do not use this.
+ * @return {Object}
+ */
+ink.SketchologyEngineWrapper.prototype.getRawEngineObject = function() {
+  throw new Error('getRawEngineObject not supported for NaCl.');
+};
+
+
+/**
+ * Generates a snapshot based on a brix document.
+ * @param {!ink.util.RealtimeDocument} brixDoc
+ * @param {function(!sketchology.proto.Snapshot)} callback
+ */
+ink.SketchologyEngineWrapper.prototype.convertBrixDocumentToSnapshot =
+    function(brixDoc, callback) {
+  var model = brixDoc.getModel();
+  var root = model.getRoot();
+  var pages = root.get('pages');
+  var page = pages.get(0);
+  if (!page) {
+    throw Error('unable to get page from brix document.');
+  }
+  var elements = page.get('elements').asArray();
+
+  var jsonElements = [];
+  for (var i = 0; i < elements.length; i++) {
+    var element = elements[i];
+    jsonElements.push({'id': element.get('id'),
+                       'proto': element.get('proto'),
+                       'transform': element.get('transform')});
+  }
+  this.brixDocuments_.push(brixDoc);
+  this.brixConversionCallbacks_.push(callback);
+  this.engineElement_.postMessage(['convertBrixElements', jsonElements]);
+};
+
+
+/**
+ * @param {!sketchology.proto.Snapshot} snapshot
+ * @param {function(boolean)} callback
+ */
+ink.SketchologyEngineWrapper.prototype.snapshotHasPendingMutations =
+    function(snapshot, callback) {
+  var bytes = this.wireSerializer_.serialize(snapshot);
+  var buf = new Uint8Array(bytes);
+  this.snapshotHasPendingMutationsCallbacks_.push(callback);
+  this.engineElement_.postMessage(['snapshotHasPendingMutations', buf.buffer]);
+};
+
+
+/**
+ * @param {!sketchology.proto.Snapshot} snapshot
+ * @param {function(sketchology.proto.MutationPacket)} callback
+ */
+ink.SketchologyEngineWrapper.prototype.extractMutationPacket =
+    function(snapshot, callback) {
+  var bytes = this.wireSerializer_.serialize(snapshot);
+  var buf = new Uint8Array(bytes);
+  this.extractMutationPacketCallbacks_.push(callback);
+  this.engineElement_.postMessage(['extractMutationPacket', buf.buffer]);
+};
+
+
+/**
+ * @param {!sketchology.proto.Snapshot} snapshot
+ * @param {function(sketchology.proto.Snapshot)} callback
+ */
+ink.SketchologyEngineWrapper.prototype.clearPendingMutations = function(
+    snapshot, callback) {
+  var bytes = this.wireSerializer_.serialize(snapshot);
+  var buf = new Uint8Array(bytes);
+  this.clearPendingMutationsCallbacks_.push(callback);
+  this.engineElement_.postMessage(['clearPendingMutations', buf.buffer]);
+};
+
+
+/**
+ * Enable or disable hardware overlay by hiding or showing a 1-pixel div over
+ * the canvas.
+ *
+ * @param {boolean} enable
+ */
+ink.SketchologyEngineWrapper.prototype.setHardwareOverlay = function(enable) {
+  document.querySelector('#ink-engine-hwoverlay').style.display =
+      enable ? 'none' : 'block';
+};
+
+
+/**
+ * Calls the given callback once all previous asynchronous engine operations
+ * have been applied.
+ * @param {!Function} callback
+ */
+ink.SketchologyEngineWrapper.prototype.flush = function(callback) {
+  var commandProto = new sketchology.proto.Command();
+  var sp = new sketchology.proto.SequencePoint();
+  sp.setId(this.nextSequencePointId_);
+  this.sequencePointCallbacks_[this.nextSequencePointId_++] = callback;
+  commandProto.setSequencePoint(sp);
+  this.handleCommand(commandProto);
+};
diff --git a/third_party/ink/template/soy/soyutils_usegoog.js b/third_party/ink/template/soy/soyutils_usegoog.js
new file mode 100644
index 0000000..8720f1ca
--- /dev/null
+++ b/third_party/ink/template/soy/soyutils_usegoog.js
@@ -0,0 +1,2401 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * 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
+ * Utility functions and classes for Soy gencode
+ *
+ * <p>
+ * This file contains utilities that should only be called by Soy-generated
+ * JS code. Please do not use these functions directly from
+ * your hand-written code. Their names all start with '$$', or exist within the
+ * soydata.VERY_UNSAFE namespace.
+ *
+ * <p>TODO(lukes): ensure that the above pattern is actually followed
+ * consistently.
+ *
+ * @author Garrett Boyer
+ * @author Mike Samuel
+ * @author Kai Huang
+ * @author Aharon Lanin
+ */
+goog.provide('soy');
+goog.provide('soy.asserts');
+goog.provide('soy.esc');
+goog.provide('soydata');
+goog.provide('soydata.SanitizedHtml');
+goog.provide('soydata.VERY_UNSAFE');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.debug');
+goog.require('goog.format');
+goog.require('goog.html.SafeHtml');
+goog.require('goog.html.SafeScript');
+goog.require('goog.html.SafeStyle');
+goog.require('goog.html.SafeStyleSheet');
+goog.require('goog.html.SafeUrl');
+goog.require('goog.html.TrustedResourceUrl');
+goog.require('goog.html.uncheckedconversions');
+goog.require('goog.i18n.BidiFormatter');
+goog.require('goog.i18n.bidi');
+goog.require('goog.object');
+goog.require('goog.soy.data.SanitizedContent');
+goog.require('goog.soy.data.SanitizedContentKind');
+goog.require('goog.soy.data.SanitizedCss');
+goog.require('goog.soy.data.SanitizedHtml');
+goog.require('goog.soy.data.SanitizedHtmlAttribute');
+goog.require('goog.soy.data.SanitizedJs');
+goog.require('goog.soy.data.SanitizedStyle');
+goog.require('goog.soy.data.SanitizedTrustedResourceUri');
+goog.require('goog.soy.data.SanitizedUri');
+goog.require('goog.soy.data.UnsanitizedText');
+goog.require('goog.string');
+goog.require('goog.string.Const');
+
+// -----------------------------------------------------------------------------
+// soydata: Defines typed strings, e.g. an HTML string {@code "a<b>c"} is
+// semantically distinct from the plain text string {@code "a<b>c"} and smart
+// templates can take that distinction into account.
+
+/**
+ * Checks whether a given value is of a given content kind.
+ *
+ * @param {*} value The value to be examined.
+ * @param {goog.soy.data.SanitizedContentKind} contentKind The desired content
+ *     kind.
+ * @return {boolean} Whether the given value is of the given kind.
+ * @private
+ */
+soydata.isContentKind_ = function(value, contentKind) {
+  // TODO(aharon): This function should really include the assert on
+  // value.constructor that is currently sprinkled at most of the call sites.
+  // Unfortunately, that would require a (debug-mode-only) switch statement.
+  // TODO(aharon): Perhaps we should get rid of the contentKind property
+  // altogether and only at the constructor.
+  return value != null && value.contentKind === contentKind;
+};
+
+
+/**
+ * Returns a given value's contentDir property, constrained to a
+ * goog.i18n.bidi.Dir value or null. Returns null if the value is null,
+ * undefined, a primitive or does not have a contentDir property, or the
+ * property's value is not 1 (for LTR), -1 (for RTL), or 0 (for neutral).
+ *
+ * @param {*} value The value whose contentDir property, if any, is to
+ *     be returned.
+ * @return {?goog.i18n.bidi.Dir} The contentDir property.
+ */
+soydata.getContentDir = function(value) {
+  if (value != null) {
+    switch (value.contentDir) {
+      case goog.i18n.bidi.Dir.LTR:
+        return goog.i18n.bidi.Dir.LTR;
+      case goog.i18n.bidi.Dir.RTL:
+        return goog.i18n.bidi.Dir.RTL;
+      case goog.i18n.bidi.Dir.NEUTRAL:
+        return goog.i18n.bidi.Dir.NEUTRAL;
+    }
+  }
+  return null;
+};
+
+
+/**
+ * This class is only a holder for {@code soydata.SanitizedHtml.from}. Do not
+ * instantiate or extend it. Use {@code goog.soy.data.SanitizedHtml} instead.
+ *
+ * @constructor
+ * @extends {goog.soy.data.SanitizedHtml}
+ * @abstract
+ */
+soydata.SanitizedHtml = function() {
+  soydata.SanitizedHtml.base(this, 'constructor');  // Throws an exception.
+};
+goog.inherits(soydata.SanitizedHtml, goog.soy.data.SanitizedHtml);
+
+/**
+ * Returns a SanitizedHtml object for a particular value. The content direction
+ * is preserved.
+ *
+ * This HTML-escapes the value unless it is already SanitizedHtml or SafeHtml.
+ *
+ * @param {*} value The value to convert. If it is already a SanitizedHtml
+ *     object, it is left alone.
+ * @return {!goog.soy.data.SanitizedHtml} A SanitizedHtml object derived from
+ *     the stringified value. It is escaped unless the input is SanitizedHtml or
+ *     SafeHtml.
+ */
+soydata.SanitizedHtml.from = function(value) {
+  // The check is soydata.isContentKind_() inlined for performance.
+  if (value != null &&
+      value.contentKind === goog.soy.data.SanitizedContentKind.HTML) {
+    goog.asserts.assert(value.constructor === goog.soy.data.SanitizedHtml);
+    return /** @type {!goog.soy.data.SanitizedHtml} */ (value);
+  }
+  if (value instanceof goog.html.SafeHtml) {
+    return soydata.VERY_UNSAFE.ordainSanitizedHtml(
+        goog.html.SafeHtml.unwrap(value), value.getDirection());
+  }
+  return soydata.VERY_UNSAFE.ordainSanitizedHtml(
+      soy.esc.$$escapeHtmlHelper(String(value)), soydata.getContentDir(value));
+};
+
+
+/**
+ * Empty string, used as a type in Soy templates.
+ * @enum {string}
+ * @private
+ */
+soydata.$$EMPTY_STRING_ = {
+  VALUE: ''
+};
+
+
+/**
+ * Creates a factory for SanitizedContent types.
+ *
+ * This is a hack so that the soydata.VERY_UNSAFE.ordainSanitized* can
+ * instantiate Sanitized* classes, without making the Sanitized* constructors
+ * publicly usable. Requiring all construction to use the VERY_UNSAFE names
+ * helps callers and their reviewers easily tell that creating SanitizedContent
+ * is not always safe and calls for careful review.
+ *
+ * @param {function(new: T)} ctor A constructor.
+ * @return {!function(*, ?goog.i18n.bidi.Dir=): T} A factory that takes
+ *     content and an optional content direction and returns a new instance. If
+ *     the content direction is undefined, ctor.prototype.contentDir is used.
+ * @template T
+ * @private
+ */
+soydata.$$makeSanitizedContentFactory_ = function(ctor) {
+  /**
+   * @param {string} content
+   * @constructor
+   * @extends {goog.soy.data.SanitizedContent}
+   */
+  function InstantiableCtor(content) {
+    /** @override */
+    this.content = content;
+  }
+  InstantiableCtor.prototype = ctor.prototype;
+  /**
+   * Creates a ctor-type SanitizedContent instance.
+   *
+   * @param {*} content The content to put in the instance.
+   * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction. If
+   *     undefined, ctor.prototype.contentDir is used.
+   * @return {!goog.soy.data.SanitizedContent} The new instance. It is actually
+   *     of type T above (ctor's type, a descendant of SanitizedContent), but
+   *     there is no way to express that here.
+   */
+  function sanitizedContentFactory(content, opt_contentDir) {
+    var result = new InstantiableCtor(String(content));
+    if (opt_contentDir !== undefined) {
+      result.contentDir = opt_contentDir;
+    }
+    return result;
+  }
+  return sanitizedContentFactory;
+};
+
+
+/**
+ * Creates a factory for SanitizedContent types that should always have their
+ * default directionality.
+ *
+ * This is a hack so that the soydata.VERY_UNSAFE.ordainSanitized* can
+ * instantiate Sanitized* classes, without making the Sanitized* constructors
+ * publicly usable. Requiring all construction to use the VERY_UNSAFE names
+ * helps callers and their reviewers easily tell that creating SanitizedContent
+ * is not always safe and calls for careful review.
+ *
+ * @param {function(new: T, string)} ctor A constructor.
+ * @return {!function(*): T} A factory that takes content and returns a new
+ *     instance (with default directionality, i.e. ctor.prototype.contentDir).
+ * @template T
+ * @private
+ */
+soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_ = function(ctor) {
+  /**
+   * @param {string} content
+   * @constructor
+   * @extends {goog.soy.data.SanitizedContent}
+   */
+  function InstantiableCtor(content) {
+    /** @override */
+    this.content = content;
+  }
+  InstantiableCtor.prototype = ctor.prototype;
+  /**
+   * Creates a ctor-type SanitizedContent instance.
+   *
+   * @param {*} content The content to put in the instance.
+   * @return {!goog.soy.data.SanitizedContent} The new instance. It is actually
+   *     of type T above (ctor's type, a descendant of SanitizedContent), but
+   *     there is no way to express that here.
+   */
+  function sanitizedContentFactory(content) {
+    var result = new InstantiableCtor(String(content));
+    return result;
+  }
+  return sanitizedContentFactory;
+};
+
+
+// -----------------------------------------------------------------------------
+// Sanitized content ordainers. Please use these with extreme caution (with the
+// exception of markUnsanitizedText). A good recommendation is to limit usage
+// of these to just a handful of files in your source tree where usages can be
+// carefully audited.
+
+
+/**
+ * Protects a string from being used in an noAutoescaped context.
+ *
+ * This is useful for content where there is significant risk of accidental
+ * unescaped usage in a Soy template. A great case is for user-controlled
+ * data that has historically been a source of vulernabilities.
+ *
+ * @param {*} content Text to protect.
+ * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if
+ *     unknown and thus to be estimated when necessary. Default: null.
+ * @return {!goog.soy.data.UnsanitizedText} A wrapper that is rejected by the
+ *     Soy noAutoescape print directive.
+ */
+soydata.markUnsanitizedText = function(content, opt_contentDir) {
+  return new goog.soy.data.UnsanitizedText(content, opt_contentDir);
+};
+
+
+/**
+ * Takes a leap of faith that the provided content is "safe" HTML.
+ *
+ * @param {*} content A string of HTML that can safely be embedded in
+ *     a PCDATA context in your app. If you would be surprised to find that an
+ *     HTML sanitizer produced {@code s} (e.g. it runs code or fetches bad URLs)
+ *     and you wouldn't write a template that produces {@code s} on security or
+ *     privacy grounds, then don't pass {@code s} here.
+ * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if
+ *     unknown and thus to be estimated when necessary. Default: null.
+ * @return {!goog.soy.data.SanitizedHtml} Sanitized content wrapper that
+ *     indicates to Soy not to escape when printed as HTML.
+ */
+soydata.VERY_UNSAFE.ordainSanitizedHtml =
+    soydata.$$makeSanitizedContentFactory_(goog.soy.data.SanitizedHtml);
+
+
+/**
+ * Takes a leap of faith that the provided content is "safe" (non-attacker-
+ * controlled, XSS-free) Javascript.
+ *
+ * @param {*} content Javascript source that when evaluated does not
+ *     execute any attacker-controlled scripts.
+ * @return {!goog.soy.data.SanitizedJs} Sanitized content wrapper that indicates
+ *     to Soy not to escape when printed as Javascript source.
+ */
+soydata.VERY_UNSAFE.ordainSanitizedJs =
+    soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_(
+        goog.soy.data.SanitizedJs);
+
+
+/**
+ * Takes a leap of faith that the provided content is "safe" to use as a URI
+ * in a Soy template.
+ *
+ * This creates a Soy SanitizedContent object which indicates to Soy there is
+ * no need to escape it when printed as a URI (e.g. in an href or src
+ * attribute), such as if it's already been encoded or  if it's a Javascript:
+ * URI.
+ *
+ * @param {*} content A chunk of URI that the caller knows is safe to
+ *     emit in a template.
+ * @return {!goog.soy.data.SanitizedUri} Sanitized content wrapper that
+ *     indicates to Soy not to escape or filter when printed in URI context.
+ */
+soydata.VERY_UNSAFE.ordainSanitizedUri =
+    soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_(
+        goog.soy.data.SanitizedUri);
+
+
+/**
+ * Takes a leap of faith that the provided content is "safe" to use as a
+ * TrustedResourceUri in a Soy template.
+ *
+ * This creates a Soy SanitizedContent object which indicates to Soy there is
+ * no need to filter it when printed as a TrustedResourceUri.
+ *
+ * @param {*} content A chunk of TrustedResourceUri such as that the caller
+ *     knows is safe to emit in a template.
+ * @return {!goog.soy.data.SanitizedTrustedResourceUri} Sanitized content
+ *     wrapper that indicates to Soy not to escape or filter when printed in
+ *     TrustedResourceUri context.
+ */
+soydata.VERY_UNSAFE.ordainSanitizedTrustedResourceUri =
+    soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_(
+        goog.soy.data.SanitizedTrustedResourceUri);
+
+
+/**
+ * Takes a leap of faith that the provided content is "safe" to use as an
+ * HTML attribute.
+ *
+ * @param {*} content An attribute name and value, such as
+ *     {@code dir="ltr"}.
+ * @return {!goog.soy.data.SanitizedHtmlAttribute} Sanitized content wrapper
+ *     that indicates to Soy not to escape when printed as an HTML attribute.
+ */
+soydata.VERY_UNSAFE.ordainSanitizedHtmlAttribute =
+    soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_(
+        goog.soy.data.SanitizedHtmlAttribute);
+
+
+/**
+ * Takes a leap of faith that the provided content is "safe" to use as STYLE
+ * in a style attribute.
+ *
+ * @param {*} content CSS, such as {@code color:#c3d9ff}.
+ * @return {!goog.soy.data.SanitizedStyle} Sanitized style wrapper that
+ *     indicates to Soy there is no need to escape or filter when printed in CSS
+ *     context.
+ */
+soydata.VERY_UNSAFE.ordainSanitizedStyle =
+    soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_(
+        goog.soy.data.SanitizedStyle);
+
+
+/**
+ * Takes a leap of faith that the provided content is "safe" to use as CSS
+ * in a style block.
+ *
+ * @param {*} content CSS, such as {@code color:#c3d9ff}.
+ * @return {!goog.soy.data.SanitizedCss} Sanitized CSS wrapper that indicates to
+ *     Soy there is no need to escape or filter when printed in CSS context.
+ */
+soydata.VERY_UNSAFE.ordainSanitizedCss =
+    soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_(
+        goog.soy.data.SanitizedCss);
+
+
+// -----------------------------------------------------------------------------
+// Soy-generated utilities in the soy namespace.  Contains implementations for
+// common soyfunctions (e.g. keys()) and escaping/print directives.
+
+
+/**
+ * Whether the locale is right-to-left.
+ *
+ * @type {boolean}
+ */
+soy.$$IS_LOCALE_RTL = goog.i18n.bidi.IS_RTL;
+
+
+/**
+ * Builds an augmented map. The returned map will contain mappings from both
+ * the base map and the additional map. If the same key appears in both, then
+ * the value from the additional map will be visible, while the value from the
+ * base map will be hidden. The base map will be used, but not modified.
+ *
+ * @param {!Object} baseMap The original map to augment.
+ * @param {!Object} additionalMap A map containing the additional mappings.
+ * @return {!Object} An augmented map containing both the original and
+ *     additional mappings.
+ */
+soy.$$augmentMap = function(baseMap, additionalMap) {
+  return soy.$$assignDefaults(soy.$$assignDefaults({}, additionalMap), baseMap);
+};
+
+
+/**
+ * Copies extra properties into an object if they do not already exist. The
+ * destination object is mutated in the process.
+ *
+ * @param {!Object} obj The destination object to update.
+ * @param {!Object} defaults An object with default properties to apply.
+ * @return {!Object} The destination object for convenience.
+ */
+soy.$$assignDefaults = function(obj, defaults) {
+  for (var key in defaults) {
+    if (!(key in obj)) {
+      obj[key] = defaults[key];
+    }
+  }
+
+  return obj;
+};
+
+
+/**
+ * Checks that the given map key is a string.
+ * @param {*} key Key to check.
+ * @return {string} The given key.
+ */
+soy.$$checkMapKey = function(key) {
+  // TODO: Support map literal with nonstring key.
+  if ((typeof key) != 'string') {
+    throw Error(
+        'Map literal\'s key expression must evaluate to string' +
+        ' (encountered type "' + (typeof key) + '").');
+  }
+  return key;
+};
+
+
+/**
+ * Gets the keys in a map as an array. There are no guarantees on the order.
+ * @param {Object} map The map to get the keys of.
+ * @return {!Array<string>} The array of keys in the given map.
+ */
+soy.$$getMapKeys = function(map) {
+  var mapKeys = [];
+  for (var key in map) {
+    mapKeys.push(key);
+  }
+  return mapKeys;
+};
+
+
+/**
+ * Returns the argument if it is not null.
+ *
+ * @param {T} val The value to check
+ * @return {T} val if is isn't null
+ * @template T
+ */
+soy.$$checkNotNull = function(val) {
+  if (val == null) {
+    throw Error('unexpected null value');
+  }
+  return val;
+};
+
+
+/**
+ * Parses the given string into a base 10 integer. Returns null if parse is
+ * unsuccessful.
+ * @param {string} str The string to parse
+ * @return {?number} The string parsed as a base 10 integer, or null if
+ * unsuccessful
+ */
+soy.$$parseInt = function(str) {
+  var parsed = parseInt(str, 10);
+  return isNaN(parsed) ? null : parsed;
+};
+
+
+/**
+ * Parses the given string into a float. Returns null if parse is unsuccessful.
+ * @param {string} str The string to parse
+ * @return {?number} The string parsed as a float, or null if unsuccessful.
+ */
+soy.$$parseFloat = function(str) {
+  var parsed = parseFloat(str);
+  return isNaN(parsed) ? null : parsed;
+};
+
+
+/**
+ * Gets a consistent unique id for the given delegate template name. Two calls
+ * to this function will return the same id if and only if the input names are
+ * the same.
+ *
+ * <p> Important: This function must always be called with a string constant.
+ *
+ * <p> If Closure Compiler is not being used, then this is just this identity
+ * function. If Closure Compiler is being used, then each call to this function
+ * will be replaced with a short string constant, which will be consistent per
+ * input name.
+ *
+ * @param {string} delTemplateName The delegate template name for which to get a
+ *     consistent unique id.
+ * @return {string} A unique id that is consistent per input name.
+ *
+ * @idGenerator {consistent}
+ */
+soy.$$getDelTemplateId = function(delTemplateName) {
+  return delTemplateName;
+};
+
+
+/**
+ * Map from registered delegate template key to the priority of the
+ * implementation.
+ * @type {Object}
+ * @private
+ */
+soy.$$DELEGATE_REGISTRY_PRIORITIES_ = {};
+
+/**
+ * Map from registered delegate template key to the implementation function.
+ * @type {Object}
+ * @private
+ */
+soy.$$DELEGATE_REGISTRY_FUNCTIONS_ = {};
+
+
+/**
+ * Registers a delegate implementation. If the same delegate template key (id
+ * and variant) has been registered previously, then priority values are
+ * compared and only the higher priority implementation is stored (if
+ * priorities are equal, an error is thrown).
+ *
+ * @param {string} delTemplateId The delegate template id.
+ * @param {string} delTemplateVariant The delegate template variant (can be
+ *     empty string).
+ * @param {number} delPriority The implementation's priority value.
+ * @param {Function} delFn The implementation function.
+ */
+soy.$$registerDelegateFn = function(
+    delTemplateId, delTemplateVariant, delPriority, delFn) {
+
+  var mapKey = 'key_' + delTemplateId + ':' + delTemplateVariant;
+  var currPriority = soy.$$DELEGATE_REGISTRY_PRIORITIES_[mapKey];
+  if (currPriority === undefined || delPriority > currPriority) {
+    // Registering new or higher-priority function: replace registry entry.
+    soy.$$DELEGATE_REGISTRY_PRIORITIES_[mapKey] = delPriority;
+    soy.$$DELEGATE_REGISTRY_FUNCTIONS_[mapKey] = delFn;
+  } else if (delPriority == currPriority) {
+    // Registering same-priority function: error.
+    throw Error(
+        'Encountered two active delegates with the same priority ("' +
+            delTemplateId + ':' + delTemplateVariant + '").');
+  } else {
+    // Registering lower-priority function: do nothing.
+  }
+};
+
+
+/**
+ * Retrieves the (highest-priority) implementation that has been registered for
+ * a given delegate template key (id and variant). If no implementation has
+ * been registered for the key, then the fallback is the same id with empty
+ * variant. If the fallback is also not registered, and allowsEmptyDefault is
+ * true, then returns an implementation that is equivalent to an empty template
+ * (i.e. rendered output would be empty string).
+ *
+ * @param {string} delTemplateId The delegate template id.
+ * @param {string} delTemplateVariant The delegate template variant (can be
+ *     empty string).
+ * @param {boolean} allowsEmptyDefault Whether to default to the empty template
+ *     function if there's no active implementation.
+ * @return {Function} The retrieved implementation function.
+ */
+soy.$$getDelegateFn = function(
+    delTemplateId, delTemplateVariant, allowsEmptyDefault) {
+
+  var delFn = soy.$$DELEGATE_REGISTRY_FUNCTIONS_[
+      'key_' + delTemplateId + ':' + delTemplateVariant];
+  if (! delFn && delTemplateVariant != '') {
+    // Fallback to empty variant.
+    delFn = soy.$$DELEGATE_REGISTRY_FUNCTIONS_['key_' + delTemplateId + ':'];
+  }
+
+  if (delFn) {
+    return delFn;
+  } else if (allowsEmptyDefault) {
+    return soy.$$EMPTY_TEMPLATE_FN_;
+  } else {
+    throw Error(
+        'Found no active impl for delegate call to "' + delTemplateId + ':' +
+            delTemplateVariant + '" (and not allowemptydefault="true").');
+  }
+};
+
+
+/**
+ * Private helper soy.$$getDelegateFn(). This is the empty template function
+ * that is returned whenever there's no delegate implementation found.
+ *
+ * @param {Object<string, *>=} opt_data
+ * @param {Object<string, *>=} opt_ijData
+ * @param {Object<string, *>=} opt_ijData_deprecated TODO(b/36644846): remove
+ * @return {string}
+ * @private
+ */
+soy.$$EMPTY_TEMPLATE_FN_ = function(
+    opt_data, opt_ijData, opt_ijData_deprecated) {
+  return '';
+};
+
+
+// -----------------------------------------------------------------------------
+// Internal sanitized content wrappers.
+
+
+/**
+ * Creates a SanitizedContent factory for SanitizedContent types for internal
+ * Soy let and param blocks.
+ *
+ * This is a hack within Soy so that SanitizedContent objects created via let
+ * and param blocks will truth-test as false if they are empty string.
+ * Tricking the Javascript runtime to treat empty SanitizedContent as falsey is
+ * not possible, and changing the Soy compiler to wrap every boolean statement
+ * for just this purpose is impractical.  Instead, we just avoid wrapping empty
+ * string as SanitizedContent, since it's a no-op for empty strings anyways.
+ *
+ * @param {function(new: T)} ctor A constructor.
+ * @return {!function(*, ?goog.i18n.bidi.Dir=): (T|soydata.$$EMPTY_STRING_)}
+ *     A factory that takes content and an optional content direction and
+ *     returns a new instance, or an empty string. If the content direction is
+ *     undefined, ctor.prototype.contentDir is used.
+ * @template T
+ * @private
+ */
+soydata.$$makeSanitizedContentFactoryForInternalBlocks_ = function(ctor) {
+  /**
+   * @param {string} content
+   * @constructor
+   * @extends {goog.soy.data.SanitizedContent}
+   */
+  function InstantiableCtor(content) {
+    /** @override */
+    this.content = content;
+  }
+  InstantiableCtor.prototype = ctor.prototype;
+  /**
+   * Creates a ctor-type SanitizedContent instance.
+   *
+   * @param {*} content The content to put in the instance.
+   * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction. If
+   *     undefined, ctor.prototype.contentDir is used.
+   * @return {!goog.soy.data.SanitizedContent|soydata.$$EMPTY_STRING_} The new
+   *     instance, or an empty string. A new instance is actually of type T
+   *     above (ctor's type, a descendant of SanitizedContent), but there's no
+   *     way to express that here.
+   */
+  function sanitizedContentFactory(content, opt_contentDir) {
+    var contentString = String(content);
+    if (!contentString) {
+      return soydata.$$EMPTY_STRING_.VALUE;
+    }
+    var result = new InstantiableCtor(contentString);
+    if (opt_contentDir !== undefined) {
+      result.contentDir = opt_contentDir;
+    }
+    return result;
+  }
+  return sanitizedContentFactory;
+};
+
+
+/**
+ * Creates a SanitizedContent factory for SanitizedContent types that should
+ * always have their default directionality for internal Soy let and param
+ * blocks.
+ *
+ * This is a hack within Soy so that SanitizedContent objects created via let
+ * and param blocks will truth-test as false if they are empty string.
+ * Tricking the Javascript runtime to treat empty SanitizedContent as falsey is
+ * not possible, and changing the Soy compiler to wrap every boolean statement
+ * for just this purpose is impractical.  Instead, we just avoid wrapping empty
+ * string as SanitizedContent, since it's a no-op for empty strings anyways.
+ *
+ * @param {function(new: T)} ctor A constructor.
+ * @return {!function(*): (T|soydata.$$EMPTY_STRING_)} A
+ *     factory that takes content and returns a
+ *     new instance (with default directionality, i.e.
+ *     ctor.prototype.contentDir), or an empty string.
+ * @template T
+ * @private
+ */
+soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_ =
+    function(ctor) {
+  /**
+   * @param {string} content
+   * @constructor
+   * @extends {goog.soy.data.SanitizedContent}
+   */
+  function InstantiableCtor(content) {
+    /** @override */
+    this.content = content;
+  }
+  InstantiableCtor.prototype = ctor.prototype;
+  /**
+   * Creates a ctor-type SanitizedContent instance.
+   *
+   * @param {*} content The content to put in the instance.
+   * @return {!goog.soy.data.SanitizedContent|soydata.$$EMPTY_STRING_} The new
+   *     instance, or an empty string. A new instance is actually of type T
+   *     above (ctor's type, a descendant of SanitizedContent), but there's no
+   *     way to express that here.
+   */
+  function sanitizedContentFactory(content) {
+    var contentString = String(content);
+    if (!contentString) {
+      return soydata.$$EMPTY_STRING_.VALUE;
+    }
+    var result = new InstantiableCtor(contentString);
+    return result;
+  }
+  return sanitizedContentFactory;
+};
+
+
+/**
+ * Creates kind="text" block contents (internal use only).
+ *
+ * @param {*} content Text.
+ * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if
+ *     unknown and thus to be estimated when necessary. Default: null.
+ * @return {!goog.soy.data.UnsanitizedText|soydata.$$EMPTY_STRING_} Wrapped result.
+ */
+soydata.$$markUnsanitizedTextForInternalBlocks = function(
+    content, opt_contentDir) {
+  var contentString = String(content);
+  if (!contentString) {
+    return soydata.$$EMPTY_STRING_.VALUE;
+  }
+  return new goog.soy.data.UnsanitizedText(contentString, opt_contentDir);
+};
+
+
+/**
+ * Creates kind="html" block contents (internal use only).
+ *
+ * @param {*} content Text.
+ * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if
+ *     unknown and thus to be estimated when necessary. Default: null.
+ * @return {!goog.soy.data.SanitizedHtml|soydata.$$EMPTY_STRING_} Wrapped
+ *     result.
+ */
+soydata.VERY_UNSAFE.$$ordainSanitizedHtmlForInternalBlocks =
+    soydata.$$makeSanitizedContentFactoryForInternalBlocks_(
+        goog.soy.data.SanitizedHtml);
+
+
+/**
+ * Creates kind="js" block contents (internal use only).
+ *
+ * @param {*} content Text.
+ * @return {!goog.soy.data.SanitizedJs|soydata.$$EMPTY_STRING_} Wrapped result.
+ */
+soydata.VERY_UNSAFE.$$ordainSanitizedJsForInternalBlocks =
+    soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_(
+        goog.soy.data.SanitizedJs);
+
+
+/**
+ * Creates kind="trustedResourceUri" block contents (internal use only).
+ *
+ * @param {*} content Text.
+ * @return {goog.soy.data.SanitizedTrustedResourceUri|soydata.$$EMPTY_STRING_}
+ *     Wrapped result.
+ */
+soydata.VERY_UNSAFE.$$ordainSanitizedTrustedResourceUriForInternalBlocks =
+    soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_(
+        goog.soy.data.SanitizedTrustedResourceUri);
+
+
+/**
+ * Creates kind="uri" block contents (internal use only).
+ *
+ * @param {*} content Text.
+ * @return {goog.soy.data.SanitizedUri|soydata.$$EMPTY_STRING_} Wrapped result.
+ */
+soydata.VERY_UNSAFE.$$ordainSanitizedUriForInternalBlocks =
+    soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_(
+        goog.soy.data.SanitizedUri);
+
+
+/**
+ * Creates kind="attributes" block contents (internal use only).
+ *
+ * @param {*} content Text.
+ * @return {goog.soy.data.SanitizedHtmlAttribute|soydata.$$EMPTY_STRING_}
+ *     Wrapped result.
+ */
+soydata.VERY_UNSAFE.$$ordainSanitizedAttributesForInternalBlocks =
+    soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_(
+        goog.soy.data.SanitizedHtmlAttribute);
+
+
+/**
+ * Creates kind="style" block contents (internal use only).
+ *
+ * @param {*} content Text.
+ * @return {goog.soy.data.SanitizedStyle|soydata.$$EMPTY_STRING_} Wrapped
+ *     result.
+ */
+soydata.VERY_UNSAFE.$$ordainSanitizedStyleForInternalBlocks =
+    soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_(
+        goog.soy.data.SanitizedStyle);
+
+
+/**
+ * Creates kind="css" block contents (internal use only).
+ *
+ * @param {*} content Text.
+ * @return {goog.soy.data.SanitizedCss|soydata.$$EMPTY_STRING_} Wrapped result.
+ */
+soydata.VERY_UNSAFE.$$ordainSanitizedCssForInternalBlocks =
+    soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_(
+        goog.soy.data.SanitizedCss);
+
+
+// -----------------------------------------------------------------------------
+// Escape/filter/normalize.
+
+
+/**
+ * Returns a SanitizedHtml object for a particular value. The content direction
+ * is preserved.
+ *
+ * This HTML-escapes the value unless it is already SanitizedHtml. Escapes
+ * double quote '"' in addition to '&', '<', and '>' so that a string can be
+ * included in an HTML tag attribute value within double quotes.
+ *
+ * @param {*} value The value to convert. If it is already a SanitizedHtml
+ *     object, it is left alone.
+ * @return {!goog.soy.data.SanitizedHtml} An escaped version of value.
+ */
+soy.$$escapeHtml = function(value) {
+  return soydata.SanitizedHtml.from(value);
+};
+
+
+/**
+ * Strips unsafe tags to convert a string of untrusted HTML into HTML that
+ * is safe to embed. The content direction is preserved.
+ *
+ * @param {?} value The string-like value to be escaped. May not be a string,
+ *     but the value will be coerced to a string.
+ * @param {Array<string>=} opt_safeTags Additional tag names to whitelist.
+ * @return {!goog.soy.data.SanitizedHtml} A sanitized and normalized version of
+ *     value.
+ */
+soy.$$cleanHtml = function(value, opt_safeTags) {
+  if (soydata.isContentKind_(value, goog.soy.data.SanitizedContentKind.HTML)) {
+    goog.asserts.assert(value.constructor === goog.soy.data.SanitizedHtml);
+    return /** @type {!goog.soy.data.SanitizedHtml} */ (value);
+  }
+  var tagWhitelist;
+  if (opt_safeTags) {
+    tagWhitelist = goog.object.createSet(opt_safeTags);
+    goog.object.extend(tagWhitelist, soy.esc.$$SAFE_TAG_WHITELIST_);
+  } else {
+    tagWhitelist = soy.esc.$$SAFE_TAG_WHITELIST_;
+  }
+  return soydata.VERY_UNSAFE.ordainSanitizedHtml(
+      soy.$$stripHtmlTags(value, tagWhitelist), soydata.getContentDir(value));
+};
+
+
+/**
+ * Escapes HTML, except preserves entities.
+ *
+ * Used mainly internally for escaping message strings in attribute and rcdata
+ * context, where we explicitly want to preserve any existing entities.
+ *
+ * @param {*} value Value to normalize.
+ * @return {string} A value safe to insert in HTML without any quotes or angle
+ *     brackets.
+ */
+soy.$$normalizeHtml = function(value) {
+  return soy.esc.$$normalizeHtmlHelper(value);
+};
+
+
+/**
+ * Escapes HTML special characters in a string so that it can be embedded in
+ * RCDATA.
+ * <p>
+ * Escapes HTML special characters so that the value will not prematurely end
+ * the body of a tag like {@code <textarea>} or {@code <title>}. RCDATA tags
+ * cannot contain other HTML entities, so it is not strictly necessary to escape
+ * HTML special characters except when part of that text looks like an HTML
+ * entity or like a close tag : {@code </textarea>}.
+ * <p>
+ * Will normalize known safe HTML to make sure that sanitized HTML (which could
+ * contain an innocuous {@code </textarea>} don't prematurely end an RCDATA
+ * element.
+ *
+ * @param {?} value The string-like value to be escaped. May not be a string,
+ *     but the value will be coerced to a string.
+ * @return {string} An escaped version of value.
+ */
+soy.$$escapeHtmlRcdata = function(value) {
+  if (soydata.isContentKind_(value, goog.soy.data.SanitizedContentKind.HTML)) {
+    goog.asserts.assert(value.constructor === goog.soy.data.SanitizedHtml);
+    return soy.esc.$$normalizeHtmlHelper(value.getContent());
+  }
+  return soy.esc.$$escapeHtmlHelper(value);
+};
+
+
+/**
+ * Matches any/only HTML5 void elements' start tags.
+ * See http://www.w3.org/TR/html-markup/syntax.html#syntax-elements
+ * @type {RegExp}
+ * @private
+ */
+soy.$$HTML5_VOID_ELEMENTS_ = new RegExp(
+    '^<(?:area|base|br|col|command|embed|hr|img|input' +
+    '|keygen|link|meta|param|source|track|wbr)\\b');
+
+
+/**
+ * Removes HTML tags from a string of known safe HTML.
+ * If opt_tagWhitelist is not specified or is empty, then
+ * the result can be used as an attribute value.
+ *
+ * @param {*} value The HTML to be escaped. May not be a string, but the
+ *     value will be coerced to a string.
+ * @param {Object<string, boolean>=} opt_tagWhitelist Has an own property whose
+ *     name is a lower-case tag name and whose value is {@code 1} for
+ *     each element that is allowed in the output.
+ * @return {string} A representation of value without disallowed tags,
+ *     HTML comments, or other non-text content.
+ */
+soy.$$stripHtmlTags = function(value, opt_tagWhitelist) {
+  if (!opt_tagWhitelist) {
+    // If we have no white-list, then use a fast track which elides all tags.
+    return String(value).replace(soy.esc.$$HTML_TAG_REGEX_, '')
+        // This is just paranoia since callers should normalize the result
+        // anyway, but if they didn't, it would be necessary to ensure that
+        // after the first replace non-tag uses of < do not recombine into
+        // tags as in "<<foo>script>alert(1337)</<foo>script>".
+        .replace(soy.esc.$$LT_REGEX_, '&lt;');
+  }
+
+  // Escapes '[' so that we can use [123] below to mark places where tags
+  // have been removed.
+  var html = String(value).replace(/\[/g, '&#91;');
+
+  // Consider all uses of '<' and replace whitelisted tags with markers like
+  // [1] which are indices into a list of approved tag names.
+  // Replace all other uses of < and > with entities.
+  var tags = [];
+  var attrs = [];
+  html = html.replace(
+    soy.esc.$$HTML_TAG_REGEX_,
+    function(tok, tagName) {
+      if (tagName) {
+        tagName = tagName.toLowerCase();
+        if (opt_tagWhitelist.hasOwnProperty(tagName) &&
+            opt_tagWhitelist[tagName]) {
+          var isClose = tok.charAt(1) == '/';
+          var index = tags.length;
+          var start = '</';
+          var attributes = '';
+          if (!isClose) {
+            start = '<';
+            var match;
+            while ((match = soy.esc.$$HTML_ATTRIBUTE_REGEX_.exec(tok))) {
+              if (match[1] && match[1].toLowerCase() == 'dir') {
+                var dir = match[2];
+                if (dir) {
+                  if (dir.charAt(0) == '\'' || dir.charAt(0) == '"') {
+                    dir = dir.substr(1, dir.length - 2);
+                  }
+                  dir = dir.toLowerCase();
+                  if (dir == 'ltr' || dir == 'rtl' || dir == 'auto') {
+                    attributes = ' dir="' + dir + '"';
+                  }
+                }
+                break;
+              }
+            }
+            soy.esc.$$HTML_ATTRIBUTE_REGEX_.lastIndex = 0;
+          }
+          tags[index] = start + tagName + '>';
+          attrs[index] = attributes;
+          return '[' + index + ']';
+        }
+      }
+      return '';
+    });
+
+  // Escape HTML special characters. Now there are no '<' in html that could
+  // start a tag.
+  html = soy.esc.$$normalizeHtmlHelper(html);
+
+  var finalCloseTags = soy.$$balanceTags_(tags);
+
+  // Now html contains no tags or less-than characters that could become
+  // part of a tag via a replacement operation and tags only contains
+  // approved tags.
+  // Reinsert the white-listed tags.
+  html = html.replace(/\[(\d+)\]/g, function(_, index) {
+    if (attrs[index] && tags[index]) {
+      return tags[index].substr(0, tags[index].length - 1) + attrs[index] + '>';
+    }
+    return tags[index];
+  });
+
+  // Close any still open tags.
+  // This prevents unclosed formatting elements like <ol> and <table> from
+  // breaking the layout of containing HTML.
+  return html + finalCloseTags;
+};
+
+
+/**
+ * Make sure that tag boundaries are not broken by Safe CSS when embedded in a
+ * {@code <style>} element.
+ * @param {string} css
+ * @return {string}
+ * @private
+ */
+soy.$$embedCssIntoHtml_ = function(css) {
+  // Port of a method of the same name in
+  // com.google.template.soy.shared.restricted.Sanitizers
+  return css.replace(/<\//g, '<\\/').replace(/\]\]>/g, ']]\\>');
+};
+
+
+/**
+ * Throw out any close tags that don't correspond to start tags.
+ * If {@code <table>} is used for formatting, embedded HTML shouldn't be able
+ * to use a mismatched {@code </table>} to break page layout.
+ *
+ * @param {Array<string>} tags Array of open/close tags (e.g. '<p>', '</p>')
+ *    that will be modified in place to be either an open tag, one or more close
+ *    tags concatenated, or the empty string.
+ * @return {string} zero or more closed tags that close all elements that are
+ *    opened in tags but not closed.
+ * @private
+ */
+soy.$$balanceTags_ = function(tags) {
+  var open = [];
+  for (var i = 0, n = tags.length; i < n; ++i) {
+    var tag = tags[i];
+    if (tag.charAt(1) == '/') {
+      var openTagIndex = goog.array.lastIndexOf(open, tag);
+      if (openTagIndex < 0) {
+        tags[i] = '';  // Drop close tag with no corresponding open tag.
+      } else {
+        tags[i] = open.slice(openTagIndex).reverse().join('');
+        open.length = openTagIndex;
+      }
+    } else if (tag == '<li>' &&
+        goog.array.lastIndexOf(open, '</ol>') < 0 &&
+        goog.array.lastIndexOf(open, '</ul>') < 0) {
+      // Drop <li> if it isn't nested in a parent <ol> or <ul>.
+      tags[i] = '';
+    } else if (!soy.$$HTML5_VOID_ELEMENTS_.test(tag)) {
+      open.push('</' + tag.substring(1));
+    }
+  }
+  return open.reverse().join('');
+};
+
+
+/**
+ * Escapes HTML special characters in an HTML attribute value.
+ *
+ * @param {?} value The HTML to be escaped. May not be a string, but the
+ *     value will be coerced to a string.
+ * @return {string} An escaped version of value.
+ */
+soy.$$escapeHtmlAttribute = function(value) {
+  // NOTE: We don't accept ATTRIBUTES here because ATTRIBUTES is actually not
+  // the attribute value context, but instead k/v pairs.
+  if (soydata.isContentKind_(value, goog.soy.data.SanitizedContentKind.HTML)) {
+    // NOTE: After removing tags, we also escape quotes ("normalize") so that
+    // the HTML can be embedded in attribute context.
+    goog.asserts.assert(value.constructor === goog.soy.data.SanitizedHtml);
+    return soy.esc.$$normalizeHtmlHelper(
+        soy.$$stripHtmlTags(value.getContent()));
+  }
+  return soy.esc.$$escapeHtmlHelper(value);
+};
+
+
+/**
+ * Escapes HTML special characters in a string including space and other
+ * characters that can end an unquoted HTML attribute value.
+ *
+ * @param {?} value The HTML to be escaped. May not be a string, but the
+ *     value will be coerced to a string.
+ * @return {string} An escaped version of value.
+ */
+soy.$$escapeHtmlAttributeNospace = function(value) {
+  if (soydata.isContentKind_(value, goog.soy.data.SanitizedContentKind.HTML)) {
+    goog.asserts.assert(value.constructor === goog.soy.data.SanitizedHtml);
+    return soy.esc.$$normalizeHtmlNospaceHelper(
+        soy.$$stripHtmlTags(value.getContent()));
+  }
+  return soy.esc.$$escapeHtmlNospaceHelper(value);
+};
+
+
+/**
+ * Filters out strings that cannot be a substring of a valid HTML attribute.
+ *
+ * Note the input is expected to be key=value pairs.
+ *
+ * @param {?} value The value to escape. May not be a string, but the value
+ *     will be coerced to a string.
+ * @return {string} A valid HTML attribute name part or name/value pair.
+ *     {@code "zSoyz"} if the input is invalid.
+ */
+soy.$$filterHtmlAttributes = function(value) {
+  // NOTE: Explicitly no support for SanitizedContentKind.HTML, since that is
+  // meaningless in this context, which is generally *between* html attributes.
+  if (soydata.isContentKind_(
+    value, goog.soy.data.SanitizedContentKind.ATTRIBUTES)) {
+    goog.asserts.assert(
+        value.constructor === goog.soy.data.SanitizedHtmlAttribute);
+    // Add a space at the end to ensure this won't get merged into following
+    // attributes, unless the interpretation is unambiguous (ending with quotes
+    // or a space).
+    return value.getContent().replace(/([^"'\s])$/, '$1 ');
+  }
+  // TODO: Dynamically inserting attributes that aren't marked as trusted is
+  // probably unnecessary.  Any filtering done here will either be inadequate
+  // for security or not flexible enough.  Having clients use kind="attributes"
+  // in parameters seems like a wiser idea.
+  return soy.esc.$$filterHtmlAttributesHelper(value);
+};
+
+
+/**
+ * Filters out strings that cannot be a substring of a valid HTML element name.
+ *
+ * @param {*} value The value to escape. May not be a string, but the value
+ *     will be coerced to a string.
+ * @return {string} A valid HTML element name part.
+ *     {@code "zSoyz"} if the input is invalid.
+ */
+soy.$$filterHtmlElementName = function(value) {
+  // NOTE: We don't accept any SanitizedContent here. HTML indicates valid
+  // PCDATA, not tag names. A sloppy developer shouldn't be able to cause an
+  // exploit:
+  // ... {let userInput}script src=http://evil.com/evil.js{/let} ...
+  // ... {param tagName kind="html"}{$userInput}{/param} ...
+  // ... <{$tagName}>Hello World</{$tagName}>
+  return soy.esc.$$filterHtmlElementNameHelper(value);
+};
+
+
+/**
+ * Escapes characters in the value to make it valid content for a JS string
+ * literal.
+ *
+ * @param {*} value The value to escape. May not be a string, but the value
+ *     will be coerced to a string.
+ * @return {string} An escaped version of value.
+ */
+soy.$$escapeJsString = function(value) {
+  return soy.esc.$$escapeJsStringHelper(value);
+};
+
+
+/**
+ * Encodes a value as a JavaScript literal.
+ *
+ * @param {*} value The value to escape. May not be a string, but the value
+ *     will be coerced to a string.
+ * @return {string} A JavaScript code representation of the input.
+ */
+soy.$$escapeJsValue = function(value) {
+  // We surround values with spaces so that they can't be interpolated into
+  // identifiers by accident.
+  // We could use parentheses but those might be interpreted as a function call.
+  if (value == null) {  // Intentionally matches undefined.
+    // Java returns null from maps where there is no corresponding key while
+    // JS returns undefined.
+    // We always output null for compatibility with Java which does not have a
+    // distinct undefined value.
+    return ' null ';
+  }
+  if (soydata.isContentKind_(value, goog.soy.data.SanitizedContentKind.JS)) {
+    goog.asserts.assert(value.constructor === goog.soy.data.SanitizedJs);
+    return value.getContent();
+  }
+  if (value instanceof goog.html.SafeScript) {
+    return goog.html.SafeScript.unwrap(value);
+  }
+  switch (typeof value) {
+    case 'boolean': case 'number':
+      return ' ' + value + ' ';
+    default:
+      return "'" + soy.esc.$$escapeJsStringHelper(String(value)) + "'";
+  }
+};
+
+
+/**
+ * Escapes characters in the string to make it valid content for a JS regular
+ * expression literal.
+ *
+ * @param {*} value The value to escape. May not be a string, but the value
+ *     will be coerced to a string.
+ * @return {string} An escaped version of value.
+ */
+soy.$$escapeJsRegex = function(value) {
+  return soy.esc.$$escapeJsRegexHelper(value);
+};
+
+
+/**
+ * Matches all URI mark characters that conflict with HTML attribute delimiters
+ * or that cannot appear in a CSS uri.
+ * From <a href="http://www.w3.org/TR/CSS2/grammar.html">G.2: CSS grammar</a>
+ * <pre>
+ *     url        ([!#$%&*-~]|{nonascii}|{escape})*
+ * </pre>
+ *
+ * @type {RegExp}
+ * @private
+ */
+soy.$$problematicUriMarks_ = /['()]/g;
+
+/**
+ * @param {string} ch A single character in {@link soy.$$problematicUriMarks_}.
+ * @return {string}
+ * @private
+ */
+soy.$$pctEncode_ = function(ch) {
+  return '%' + ch.charCodeAt(0).toString(16);
+};
+
+/**
+ * Escapes a string so that it can be safely included in a URI.
+ *
+ * @param {*} value The value to escape. May not be a string, but the value
+ *     will be coerced to a string.
+ * @return {string} An escaped version of value.
+ */
+soy.$$escapeUri = function(value) {
+  // NOTE: We don't check for SanitizedUri or SafeUri, because just because
+  // something is already a valid complete URL doesn't mean we don't want to
+  // encode it as a component.  For example, it would be bad if
+  // ?redirect={$url} didn't escape ampersands, because in that template, the
+  // continue URL should be treated as a single unit.
+
+  // Apostophes and parentheses are not matched by encodeURIComponent.
+  // They are technically special in URIs, but only appear in the obsolete mark
+  // production in Appendix D.2 of RFC 3986, so can be encoded without changing
+  // semantics.
+  var encoded = soy.esc.$$escapeUriHelper(value);
+  soy.$$problematicUriMarks_.lastIndex = 0;
+  if (soy.$$problematicUriMarks_.test(encoded)) {
+    return encoded.replace(soy.$$problematicUriMarks_, soy.$$pctEncode_);
+  }
+  return encoded;
+};
+
+
+/**
+ * Removes rough edges from a URI by escaping any raw HTML/JS string delimiters.
+ *
+ * @param {*} value The value to escape. May not be a string, but the value
+ *     will be coerced to a string.
+ * @return {string} An escaped version of value.
+ */
+soy.$$normalizeUri = function(value) {
+  return soy.esc.$$normalizeUriHelper(value);
+};
+
+
+/**
+ * Vets a URI's protocol and removes rough edges from a URI by escaping
+ * any raw HTML/JS string delimiters.
+ *
+ * @param {?} value The value to escape. May not be a string, but the value
+ *     will be coerced to a string.
+ * @return {string} An escaped version of value.
+ */
+soy.$$filterNormalizeUri = function(value) {
+  if (soydata.isContentKind_(value, goog.soy.data.SanitizedContentKind.URI)) {
+    goog.asserts.assert(value.constructor === goog.soy.data.SanitizedUri);
+    return soy.$$normalizeUri(value);
+  }
+  if (soydata.isContentKind_(value,
+      goog.soy.data.SanitizedContentKind.TRUSTED_RESOURCE_URI)) {
+    goog.asserts.assert(
+        value.constructor === goog.soy.data.SanitizedTrustedResourceUri);
+    return soy.$$normalizeUri(value);
+  }
+  if (value instanceof goog.html.SafeUrl) {
+    return soy.$$normalizeUri(goog.html.SafeUrl.unwrap(value));
+  }
+  if (value instanceof goog.html.TrustedResourceUrl) {
+    return soy.$$normalizeUri(goog.html.TrustedResourceUrl.unwrap(value));
+  }
+  return soy.esc.$$filterNormalizeUriHelper(value);
+};
+
+
+/**
+ * Vets a URI for usage as an image source.
+ *
+ * @param {?} value The value to filter. Might not be a string, but the value
+ *     will be coerced to a string.
+ * @return {string} An escaped version of value.
+ */
+soy.$$filterNormalizeMediaUri = function(value) {
+  // Image URIs are filtered strictly more loosely than other types of URIs.
+  // TODO(shwetakarwa): Add tests for this in soyutils_test_helper while adding
+  // tests for filterTrustedResourceUri.
+  if (soydata.isContentKind_(value, goog.soy.data.SanitizedContentKind.URI)) {
+    goog.asserts.assert(value.constructor === goog.soy.data.SanitizedUri);
+    return soy.$$normalizeUri(value);
+  }
+  if (soydata.isContentKind_(value,
+      goog.soy.data.SanitizedContentKind.TRUSTED_RESOURCE_URI)) {
+    goog.asserts.assert(
+        value.constructor === goog.soy.data.SanitizedTrustedResourceUri);
+    return soy.$$normalizeUri(value);
+  }
+  if (value instanceof goog.html.SafeUrl) {
+    return soy.$$normalizeUri(goog.html.SafeUrl.unwrap(value));
+  }
+  if (value instanceof goog.html.TrustedResourceUrl) {
+    return soy.$$normalizeUri(goog.html.TrustedResourceUrl.unwrap(value));
+  }
+  return soy.esc.$$filterNormalizeMediaUriHelper(value);
+};
+
+
+/**
+ * Vets a URI for usage as a resource. Makes sure the input value is a compile
+ * time constant or a TrustedResouce not in attacker's control.
+ *
+ * @param {?} value The value to filter.
+ * @return {string} The value content.
+ */
+soy.$$filterTrustedResourceUri = function(value) {
+  if (soydata.isContentKind_(value,
+      goog.soy.data.SanitizedContentKind.TRUSTED_RESOURCE_URI)) {
+    goog.asserts.assert(
+        value.constructor === goog.soy.data.SanitizedTrustedResourceUri);
+    return value.getContent();
+  }
+  if (value instanceof goog.html.TrustedResourceUrl) {
+    return goog.html.TrustedResourceUrl.unwrap(value);
+  }
+  goog.asserts.fail('Bad value `%s` for |filterTrustedResourceUri',
+      [String(value)]);
+  return 'about:invalid#zSoyz';
+};
+
+
+/**
+ * For any resource string/variable which has
+ * |blessStringAsTrustedResuorceUrlForLegacy directive return the value as is.
+ *
+ * @param {*} value The value to be blessed. Might not be a string
+ * @return {*} value Return current value.
+ */
+soy.$$blessStringAsTrustedResourceUrlForLegacy = function(value) {
+  return value;
+};
+
+
+/**
+ * Allows only data-protocol image URI's.
+ *
+ * @param {*} value The value to process. May not be a string, but the value
+ *     will be coerced to a string.
+ * @return {!goog.soy.data.SanitizedUri} An escaped version of value.
+ */
+soy.$$filterImageDataUri = function(value) {
+  // NOTE: Even if it's a SanitizedUri, we will still filter it.
+  return soydata.VERY_UNSAFE.ordainSanitizedUri(
+      soy.esc.$$filterImageDataUriHelper(value));
+};
+
+
+/**
+ * Allows only tel URIs.
+ *
+ * @param {*} value The value to process. May not be a string, but the value
+ *     will be coerced to a string.
+ * @return {!goog.soy.data.SanitizedUri} An escaped version of value.
+ */
+soy.$$filterTelUri = function(value) {
+  // NOTE: Even if it's a SanitizedUri, we will still filter it.
+  return soydata.VERY_UNSAFE.ordainSanitizedUri(
+      soy.esc.$$filterTelUriHelper(value));
+};
+
+
+/**
+ * Escapes a string so it can safely be included inside a quoted CSS string.
+ *
+ * @param {*} value The value to escape. May not be a string, but the value
+ *     will be coerced to a string.
+ * @return {string} An escaped version of value.
+ */
+soy.$$escapeCssString = function(value) {
+  return soy.esc.$$escapeCssStringHelper(value);
+};
+
+
+/**
+ * Encodes a value as a CSS identifier part, keyword, or quantity.
+ *
+ * @param {?} value The value to escape. May not be a string, but the value
+ *     will be coerced to a string.
+ * @return {string} A safe CSS identifier part, keyword, or quanitity.
+ */
+soy.$$filterCssValue = function(value) {
+  if (soydata.isContentKind_(value, goog.soy.data.SanitizedContentKind.CSS)) {
+    goog.asserts.assertInstanceof(value, goog.soy.data.SanitizedCss);
+    return soy.$$embedCssIntoHtml_(value.getContent());
+  }
+  // Uses == to intentionally match null and undefined for Java compatibility.
+  if (value == null) {
+    return '';
+  }
+  if (value instanceof goog.html.SafeStyle) {
+    return soy.$$embedCssIntoHtml_(goog.html.SafeStyle.unwrap(value));
+  }
+  // Note: SoyToJsSrcCompiler uses soy.$$filterCssValue both for the contents of
+  // <style> (list of rules) and for the contents of style="" (one set of
+  // declarations). We support SafeStyleSheet here to be used inside <style> but
+  // it also wrongly allows it inside style="". We should instead change
+  // SoyToJsSrcCompiler to use a different function inside <style>.
+  if (value instanceof goog.html.SafeStyleSheet) {
+    return soy.$$embedCssIntoHtml_(goog.html.SafeStyleSheet.unwrap(value));
+  }
+  return soy.esc.$$filterCssValueHelper(value);
+};
+
+
+/**
+ * Sanity-checks noAutoescape input for explicitly tainted content.
+ *
+ * SanitizedContentKind.TEXT is used to explicitly mark input that was never
+ * meant to be used unescaped.
+ *
+ * @param {?} value The value to filter.
+ * @return {*} The value, that we dearly hope will not cause an attack.
+ */
+soy.$$filterNoAutoescape = function(value) {
+  if (soydata.isContentKind_(value, goog.soy.data.SanitizedContentKind.TEXT)) {
+    // Fail in development mode.
+    goog.asserts.fail(
+        'Tainted SanitizedContentKind.TEXT for |noAutoescape: `%s`',
+        [value.getContent()]);
+    // Return innocuous data in production.
+    return 'zSoyz';
+  }
+
+  return value;
+};
+
+
+// -----------------------------------------------------------------------------
+// Basic directives/functions.
+
+
+/**
+ * Converts \r\n, \r, and \n to <br>s
+ * @param {*} value The string in which to convert newlines.
+ * @return {string|!goog.soy.data.SanitizedHtml} A copy of {@code value} with
+ *     converted newlines. If {@code value} is SanitizedHtml, the return value
+ *     is also SanitizedHtml, of the same known directionality.
+ */
+soy.$$changeNewlineToBr = function(value) {
+  var result = goog.string.newLineToBr(String(value), false);
+  if (soydata.isContentKind_(value, goog.soy.data.SanitizedContentKind.HTML)) {
+    return soydata.VERY_UNSAFE.ordainSanitizedHtml(
+        result, soydata.getContentDir(value));
+  }
+  return result;
+};
+
+
+/**
+ * Inserts word breaks ('wbr' tags) into a HTML string at a given interval. The
+ * counter is reset if a space is encountered. Word breaks aren't inserted into
+ * HTML tags or entities. Entites count towards the character count; HTML tags
+ * do not.
+ *
+ * @param {*} value The HTML string to insert word breaks into. Can be other
+ *     types, but the value will be coerced to a string.
+ * @param {number} maxCharsBetweenWordBreaks Maximum number of non-space
+ *     characters to allow before adding a word break.
+ * @return {string|!goog.soy.data.SanitizedHtml} The string including word
+ *     breaks. If {@code value} is SanitizedHtml, the return value
+ *     is also SanitizedHtml, of the same known directionality.
+ * @deprecated The |insertWordBreaks directive is deprecated.
+ *     Prefer wrapping with CSS white-space: break-word.
+ */
+soy.$$insertWordBreaks = function(value, maxCharsBetweenWordBreaks) {
+  var result = goog.format.insertWordBreaks(
+      String(value), maxCharsBetweenWordBreaks);
+  if (soydata.isContentKind_(value, goog.soy.data.SanitizedContentKind.HTML)) {
+    return soydata.VERY_UNSAFE.ordainSanitizedHtml(
+        result, soydata.getContentDir(value));
+  }
+  return result;
+};
+
+
+/**
+ * Truncates a string to a given max length (if it's currently longer),
+ * optionally adding ellipsis at the end.
+ *
+ * @param {*} str The string to truncate. Can be other types, but the value will
+ *     be coerced to a string.
+ * @param {number} maxLen The maximum length of the string after truncation
+ *     (including ellipsis, if applicable).
+ * @param {boolean} doAddEllipsis Whether to add ellipsis if the string needs
+ *     truncation.
+ * @return {string} The string after truncation.
+ */
+soy.$$truncate = function(str, maxLen, doAddEllipsis) {
+
+  str = String(str);
+  if (str.length <= maxLen) {
+    return str;  // no need to truncate
+  }
+
+  // If doAddEllipsis, either reduce maxLen to compensate, or else if maxLen is
+  // too small, just turn off doAddEllipsis.
+  if (doAddEllipsis) {
+    if (maxLen > 3) {
+      maxLen -= 3;
+    } else {
+      doAddEllipsis = false;
+    }
+  }
+
+  // Make sure truncating at maxLen doesn't cut up a unicode surrogate pair.
+  if (soy.$$isHighSurrogate_(str.charCodeAt(maxLen - 1)) &&
+      soy.$$isLowSurrogate_(str.charCodeAt(maxLen))) {
+    maxLen -= 1;
+  }
+
+  // Truncate.
+  str = str.substring(0, maxLen);
+
+  // Add ellipsis.
+  if (doAddEllipsis) {
+    str += '...';
+  }
+
+  return str;
+};
+
+/**
+ * Private helper for $$truncate() to check whether a char is a high surrogate.
+ * @param {number} cc The codepoint to check.
+ * @return {boolean} Whether the given codepoint is a unicode high surrogate.
+ * @private
+ */
+soy.$$isHighSurrogate_ = function(cc) {
+  return 0xD800 <= cc && cc <= 0xDBFF;
+};
+
+/**
+ * Private helper for $$truncate() to check whether a char is a low surrogate.
+ * @param {number} cc The codepoint to check.
+ * @return {boolean} Whether the given codepoint is a unicode low surrogate.
+ * @private
+ */
+soy.$$isLowSurrogate_ = function(cc) {
+  return 0xDC00 <= cc && cc <= 0xDFFF;
+};
+
+
+// -----------------------------------------------------------------------------
+// Bidi directives/functions.
+
+
+/**
+ * Cache of bidi formatter by context directionality, so we don't keep on
+ * creating new objects.
+ * @type {!Object<!goog.i18n.BidiFormatter>}
+ * @private
+ */
+soy.$$bidiFormatterCache_ = {};
+
+
+/**
+ * Returns cached bidi formatter for bidiGlobalDir, or creates a new one.
+ * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
+ *     if rtl, 0 if unknown.
+ * @return {!goog.i18n.BidiFormatter} A formatter for bidiGlobalDir.
+ * @private
+ */
+soy.$$getBidiFormatterInstance_ = function(bidiGlobalDir) {
+  return soy.$$bidiFormatterCache_[bidiGlobalDir] ||
+         (soy.$$bidiFormatterCache_[bidiGlobalDir] =
+             new goog.i18n.BidiFormatter(bidiGlobalDir));
+};
+
+
+/**
+ * Estimate the overall directionality of text. If opt_isHtml, makes sure to
+ * ignore the LTR nature of the mark-up and escapes in text, making the logic
+ * suitable for HTML and HTML-escaped text.
+ * If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
+ * estimating the directionality.
+ *
+ * @param {*} text The content whose directionality is to be estimated.
+ * @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
+ *     Default: false.
+ * @return {number} 1 if text is LTR, -1 if it is RTL, and 0 if it is neutral.
+ */
+soy.$$bidiTextDir = function(text, opt_isHtml) {
+  var contentDir = soydata.getContentDir(text);
+  if (contentDir != null) {
+    return contentDir;
+  }
+  var isHtml = opt_isHtml ||
+      soydata.isContentKind_(text, goog.soy.data.SanitizedContentKind.HTML);
+  return goog.i18n.bidi.estimateDirection(text + '', isHtml);
+};
+
+
+/**
+ * Returns 'dir="ltr"' or 'dir="rtl"', depending on text's estimated
+ * directionality, if it is not the same as bidiGlobalDir.
+ * Otherwise, returns the empty string.
+ * If opt_isHtml, makes sure to ignore the LTR nature of the mark-up and escapes
+ * in text, making the logic suitable for HTML and HTML-escaped text.
+ * If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
+ * estimating the directionality.
+ *
+ * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
+ *     if rtl, 0 if unknown.
+ * @param {*} text The content whose directionality is to be estimated.
+ * @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
+ *     Default: false.
+ * @return {!goog.soy.data.SanitizedHtmlAttribute} 'dir="rtl"' for RTL text in
+ *     non-RTL context; 'dir="ltr"' for LTR text in non-LTR context;
+ *     else, the empty string.
+ */
+soy.$$bidiDirAttr = function(bidiGlobalDir, text, opt_isHtml) {
+  var formatter = soy.$$getBidiFormatterInstance_(bidiGlobalDir);
+  var contentDir = soydata.getContentDir(text);
+  if (contentDir == null) {
+    var isHtml = opt_isHtml ||
+        soydata.isContentKind_(text, goog.soy.data.SanitizedContentKind.HTML);
+    contentDir = goog.i18n.bidi.estimateDirection(text + '', isHtml);
+  }
+  return soydata.VERY_UNSAFE.ordainSanitizedHtmlAttribute(
+      formatter.knownDirAttr(contentDir));
+};
+
+
+/**
+ * Returns a Unicode BiDi mark matching bidiGlobalDir (LRM or RLM) if the
+ * directionality or the exit directionality of text are opposite to
+ * bidiGlobalDir. Otherwise returns the empty string.
+ * If opt_isHtml, makes sure to ignore the LTR nature of the mark-up and escapes
+ * in text, making the logic suitable for HTML and HTML-escaped text.
+ * If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
+ * estimating the directionality.
+ *
+ * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
+ *     if rtl, 0 if unknown.
+ * @param {*} text The content whose directionality is to be estimated.
+ * @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
+ *     Default: false.
+ * @return {string} A Unicode bidi mark matching bidiGlobalDir, or the empty
+ *     string when text's overall and exit directionalities both match
+ *     bidiGlobalDir, or bidiGlobalDir is 0 (unknown).
+ */
+soy.$$bidiMarkAfter = function(bidiGlobalDir, text, opt_isHtml) {
+  var formatter = soy.$$getBidiFormatterInstance_(bidiGlobalDir);
+  var isHtml = opt_isHtml ||
+      soydata.isContentKind_(text, goog.soy.data.SanitizedContentKind.HTML);
+  return formatter.markAfterKnownDir(soydata.getContentDir(text), text + '',
+      isHtml);
+};
+
+
+/**
+ * Returns text wrapped in a <span dir="ltr|rtl"> according to its
+ * directionality - but only if that is neither neutral nor the same as the
+ * global context. Otherwise, returns text unchanged.
+ * Always treats text as HTML/HTML-escaped, i.e. ignores mark-up and escapes
+ * when estimating text's directionality.
+ * If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
+ * estimating the directionality.
+ *
+ * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
+ *     if rtl, 0 if unknown.
+ * @param {*} text The string to be wrapped. Can be other types, but the value
+ *     will be coerced to a string.
+ * @return {!goog.soy.data.SanitizedContent|string} The wrapped text.
+ */
+soy.$$bidiSpanWrap = function(bidiGlobalDir, text) {
+  var formatter = soy.$$getBidiFormatterInstance_(bidiGlobalDir);
+
+  // We always treat the value as HTML, because span-wrapping is only useful
+  // when its output will be treated as HTML (without escaping), and because
+  // |bidiSpanWrap is not itself specified to do HTML escaping in Soy. (Both
+  // explicit and automatic HTML escaping, if any, is done before calling
+  // |bidiSpanWrap because the BidiSpanWrapDirective Java class implements
+  // SanitizedContentOperator, but this does not mean that the input has to be
+  // HTML SanitizedContent. In legacy usage, a string that is not
+  // SanitizedContent is often printed in an autoescape="false" template or by
+  // a print with a |noAutoescape, in which case our input is just SoyData.) If
+  // the output will be treated as HTML, the input had better be safe
+  // HTML/HTML-escaped (even if it isn't HTML SanitizedData), or we have an XSS
+  // opportunity and a much bigger problem than bidi garbling.
+  var html = goog.html.uncheckedconversions.
+      safeHtmlFromStringKnownToSatisfyTypeContract(
+          goog.string.Const.from(
+              'Soy |bidiSpanWrap is applied on an autoescaped text.'),
+          String(text));
+  var wrappedHtml = formatter.spanWrapSafeHtmlWithKnownDir(
+      soydata.getContentDir(text), html);
+
+  // Like other directives whose Java class implements SanitizedContentOperator,
+  // |bidiSpanWrap is called after the escaping (if any) has already been done,
+  // and thus there is no need for it to produce actual SanitizedContent.
+  return goog.html.SafeHtml.unwrap(wrappedHtml);
+};
+
+
+/**
+ * Returns text wrapped in Unicode BiDi formatting characters according to its
+ * directionality, i.e. either LRE or RLE at the beginning and PDF at the end -
+ * but only if text's directionality is neither neutral nor the same as the
+ * global context. Otherwise, returns text unchanged.
+ * Only treats SanitizedHtml as HTML/HTML-escaped, i.e. ignores mark-up
+ * and escapes when estimating text's directionality.
+ * If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
+ * estimating the directionality.
+ *
+ * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
+ *     if rtl, 0 if unknown.
+ * @param {*} text The string to be wrapped. Can be other types, but the value
+ *     will be coerced to a string.
+ * @return {!goog.soy.data.SanitizedContent|string} The wrapped string.
+ */
+soy.$$bidiUnicodeWrap = function(bidiGlobalDir, text) {
+  var formatter = soy.$$getBidiFormatterInstance_(bidiGlobalDir);
+
+  // We treat the value as HTML if and only if it says it's HTML, even though in
+  // legacy usage, we sometimes have an HTML string (not SanitizedContent) that
+  // is passed to an autoescape="false" template or a {print $foo|noAutoescape},
+  // with the output going into an HTML context without escaping. We simply have
+  // no way of knowing if this is what is happening when we get
+  // non-SanitizedContent input, and most of the time it isn't.
+  var isHtml =
+      soydata.isContentKind_(text, goog.soy.data.SanitizedContentKind.HTML);
+  var wrappedText = formatter.unicodeWrapWithKnownDir(
+      soydata.getContentDir(text), text + '', isHtml);
+
+  // Bidi-wrapping a value converts it to the context directionality. Since it
+  // does not cost us anything, we will indicate this known direction in the
+  // output SanitizedContent, even though the intended consumer of that
+  // information - a bidi wrapping directive - has already been run.
+  var wrappedTextDir = formatter.getContextDir();
+
+  // Unicode-wrapping UnsanitizedText gives UnsanitizedText.
+  // Unicode-wrapping safe HTML or JS string data gives valid, safe HTML or JS
+  // string data.
+  // ATTENTION: Do these need to be ...ForInternalBlocks()?
+  if (soydata.isContentKind_(text, goog.soy.data.SanitizedContentKind.TEXT)) {
+    return new goog.soy.data.UnsanitizedText(wrappedText, wrappedTextDir);
+  }
+  if (isHtml) {
+    return soydata.VERY_UNSAFE.ordainSanitizedHtml(wrappedText, wrappedTextDir);
+  }
+
+  // Unicode-wrapping does not conform to the syntax of the other types of
+  // content. For lack of anything better to do, we we do not declare a content
+  // kind at all by falling through to the non-SanitizedContent case below.
+  // TODO(aharon): Consider throwing a runtime error on receipt of
+  // SanitizedContent other than TEXT, HTML, or JS_STR_CHARS.
+
+  // The input was not SanitizedContent, so our output isn't SanitizedContent
+  // either.
+  return wrappedText;
+};
+
+// -----------------------------------------------------------------------------
+// Assertion methods used by runtime.
+
+/**
+ * Checks if the type assertion is true if goog.asserts.ENABLE_ASSERTS is
+ * true. Report errors on runtime types if goog.DEBUG is true.
+ * @param {boolean} condition The type check condition.
+ * @param {string} paramName The Soy name of the parameter.
+ * @param {?} param The JS object for the parameter.
+ * @param {!string} jsDocTypeStr SoyDoc type str.
+ * @return {?} the param value
+ * @throws {goog.asserts.AssertionError} When the condition evaluates to false.
+ */
+soy.asserts.assertType = function(condition, paramName, param, jsDocTypeStr) {
+  if (goog.asserts.ENABLE_ASSERTS && !condition) {
+    var msg = 'expected param ' + paramName + ' of type ' + jsDocTypeStr +
+        (goog.DEBUG ? (', but got ' + goog.debug.runtimeType(param)) : '') +
+        '.';
+    goog.asserts.fail(msg);
+  }
+  return param;
+};
+
+// -----------------------------------------------------------------------------
+// Used for inspecting Soy template information from rendered pages.
+
+/**
+ * Whether we should generate additional HTML comments.
+ * @type {boolean}
+ */
+soy.$$debugSoyTemplateInfo = false;
+
+if (goog.DEBUG) {
+  /**
+   * Configures whether we should generate additional HTML comments for
+   * inspecting Soy template information from rendered pages.
+   * @param {boolean} debugSoyTemplateInfo
+   */
+  soy.setDebugSoyTemplateInfo = function(debugSoyTemplateInfo) {
+    soy.$$debugSoyTemplateInfo = debugSoyTemplateInfo;
+  };
+}
+
+// -----------------------------------------------------------------------------
+// Generated code.
+
+
+// START GENERATED CODE FOR ESCAPERS.
+
+/**
+ * @type {function (*) : string}
+ */
+soy.esc.$$escapeHtmlHelper = function(v) {
+  return goog.string.htmlEscape(String(v));
+};
+
+/**
+ * @type {function (*) : string}
+ */
+soy.esc.$$escapeUriHelper = function(v) {
+  return goog.string.urlEncode(String(v));
+};
+
+/**
+ * Maps characters to the escaped versions for the named escape directives.
+ * @private {!Object<string, string>}
+ */
+soy.esc.$$ESCAPE_MAP_FOR_NORMALIZE_HTML__AND__ESCAPE_HTML_NOSPACE__AND__NORMALIZE_HTML_NOSPACE_ = {
+  '\x00': '\x26#0;',
+  '\x09': '\x26#9;',
+  '\x0a': '\x26#10;',
+  '\x0b': '\x26#11;',
+  '\x0c': '\x26#12;',
+  '\x0d': '\x26#13;',
+  ' ': '\x26#32;',
+  '\x22': '\x26quot;',
+  '\x26': '\x26amp;',
+  '\x27': '\x26#39;',
+  '-': '\x26#45;',
+  '\/': '\x26#47;',
+  '\x3c': '\x26lt;',
+  '\x3d': '\x26#61;',
+  '\x3e': '\x26gt;',
+  '`': '\x26#96;',
+  '\x85': '\x26#133;',
+  '\xa0': '\x26#160;',
+  '\u2028': '\x26#8232;',
+  '\u2029': '\x26#8233;'
+};
+
+/**
+ * A function that can be used with String.replace.
+ * @param {string} ch A single character matched by a compatible matcher.
+ * @return {string} A token in the output language.
+ * @private
+ */
+soy.esc.$$REPLACER_FOR_NORMALIZE_HTML__AND__ESCAPE_HTML_NOSPACE__AND__NORMALIZE_HTML_NOSPACE_ = function(ch) {
+  return soy.esc.$$ESCAPE_MAP_FOR_NORMALIZE_HTML__AND__ESCAPE_HTML_NOSPACE__AND__NORMALIZE_HTML_NOSPACE_[ch];
+};
+
+/**
+ * Maps characters to the escaped versions for the named escape directives.
+ * @private {!Object<string, string>}
+ */
+soy.esc.$$ESCAPE_MAP_FOR_ESCAPE_JS_STRING__AND__ESCAPE_JS_REGEX_ = {
+  '\x00': '\\x00',
+  '\x08': '\\x08',
+  '\x09': '\\t',
+  '\x0a': '\\n',
+  '\x0b': '\\x0b',
+  '\x0c': '\\f',
+  '\x0d': '\\r',
+  '\x22': '\\x22',
+  '$': '\\x24',
+  '\x26': '\\x26',
+  '\x27': '\\x27',
+  '(': '\\x28',
+  ')': '\\x29',
+  '*': '\\x2a',
+  '+': '\\x2b',
+  ',': '\\x2c',
+  '-': '\\x2d',
+  '.': '\\x2e',
+  '\/': '\\\/',
+  ':': '\\x3a',
+  '\x3c': '\\x3c',
+  '\x3d': '\\x3d',
+  '\x3e': '\\x3e',
+  '?': '\\x3f',
+  '\x5b': '\\x5b',
+  '\\': '\\\\',
+  '\x5d': '\\x5d',
+  '^': '\\x5e',
+  '\x7b': '\\x7b',
+  '|': '\\x7c',
+  '\x7d': '\\x7d',
+  '\x85': '\\x85',
+  '\u2028': '\\u2028',
+  '\u2029': '\\u2029'
+};
+
+/**
+ * A function that can be used with String.replace.
+ * @param {string} ch A single character matched by a compatible matcher.
+ * @return {string} A token in the output language.
+ * @private
+ */
+soy.esc.$$REPLACER_FOR_ESCAPE_JS_STRING__AND__ESCAPE_JS_REGEX_ = function(ch) {
+  return soy.esc.$$ESCAPE_MAP_FOR_ESCAPE_JS_STRING__AND__ESCAPE_JS_REGEX_[ch];
+};
+
+/**
+ * Maps characters to the escaped versions for the named escape directives.
+ * @private {!Object<string, string>}
+ */
+soy.esc.$$ESCAPE_MAP_FOR_ESCAPE_CSS_STRING_ = {
+  '\x00': '\\0 ',
+  '\x08': '\\8 ',
+  '\x09': '\\9 ',
+  '\x0a': '\\a ',
+  '\x0b': '\\b ',
+  '\x0c': '\\c ',
+  '\x0d': '\\d ',
+  '\x22': '\\22 ',
+  '\x26': '\\26 ',
+  '\x27': '\\27 ',
+  '(': '\\28 ',
+  ')': '\\29 ',
+  '*': '\\2a ',
+  '\/': '\\2f ',
+  ':': '\\3a ',
+  ';': '\\3b ',
+  '\x3c': '\\3c ',
+  '\x3d': '\\3d ',
+  '\x3e': '\\3e ',
+  '@': '\\40 ',
+  '\\': '\\5c ',
+  '\x7b': '\\7b ',
+  '\x7d': '\\7d ',
+  '\x85': '\\85 ',
+  '\xa0': '\\a0 ',
+  '\u2028': '\\2028 ',
+  '\u2029': '\\2029 '
+};
+
+/**
+ * A function that can be used with String.replace.
+ * @param {string} ch A single character matched by a compatible matcher.
+ * @return {string} A token in the output language.
+ * @private
+ */
+soy.esc.$$REPLACER_FOR_ESCAPE_CSS_STRING_ = function(ch) {
+  return soy.esc.$$ESCAPE_MAP_FOR_ESCAPE_CSS_STRING_[ch];
+};
+
+/**
+ * Maps characters to the escaped versions for the named escape directives.
+ * @private {!Object<string, string>}
+ */
+soy.esc.$$ESCAPE_MAP_FOR_NORMALIZE_URI__AND__FILTER_NORMALIZE_URI__AND__FILTER_NORMALIZE_MEDIA_URI_ = {
+  '\x00': '%00',
+  '\x01': '%01',
+  '\x02': '%02',
+  '\x03': '%03',
+  '\x04': '%04',
+  '\x05': '%05',
+  '\x06': '%06',
+  '\x07': '%07',
+  '\x08': '%08',
+  '\x09': '%09',
+  '\x0a': '%0A',
+  '\x0b': '%0B',
+  '\x0c': '%0C',
+  '\x0d': '%0D',
+  '\x0e': '%0E',
+  '\x0f': '%0F',
+  '\x10': '%10',
+  '\x11': '%11',
+  '\x12': '%12',
+  '\x13': '%13',
+  '\x14': '%14',
+  '\x15': '%15',
+  '\x16': '%16',
+  '\x17': '%17',
+  '\x18': '%18',
+  '\x19': '%19',
+  '\x1a': '%1A',
+  '\x1b': '%1B',
+  '\x1c': '%1C',
+  '\x1d': '%1D',
+  '\x1e': '%1E',
+  '\x1f': '%1F',
+  ' ': '%20',
+  '\x22': '%22',
+  '\x27': '%27',
+  '(': '%28',
+  ')': '%29',
+  '\x3c': '%3C',
+  '\x3e': '%3E',
+  '\\': '%5C',
+  '\x7b': '%7B',
+  '\x7d': '%7D',
+  '\x7f': '%7F',
+  '\x85': '%C2%85',
+  '\xa0': '%C2%A0',
+  '\u2028': '%E2%80%A8',
+  '\u2029': '%E2%80%A9',
+  '\uff01': '%EF%BC%81',
+  '\uff03': '%EF%BC%83',
+  '\uff04': '%EF%BC%84',
+  '\uff06': '%EF%BC%86',
+  '\uff07': '%EF%BC%87',
+  '\uff08': '%EF%BC%88',
+  '\uff09': '%EF%BC%89',
+  '\uff0a': '%EF%BC%8A',
+  '\uff0b': '%EF%BC%8B',
+  '\uff0c': '%EF%BC%8C',
+  '\uff0f': '%EF%BC%8F',
+  '\uff1a': '%EF%BC%9A',
+  '\uff1b': '%EF%BC%9B',
+  '\uff1d': '%EF%BC%9D',
+  '\uff1f': '%EF%BC%9F',
+  '\uff20': '%EF%BC%A0',
+  '\uff3b': '%EF%BC%BB',
+  '\uff3d': '%EF%BC%BD'
+};
+
+/**
+ * A function that can be used with String.replace.
+ * @param {string} ch A single character matched by a compatible matcher.
+ * @return {string} A token in the output language.
+ * @private
+ */
+soy.esc.$$REPLACER_FOR_NORMALIZE_URI__AND__FILTER_NORMALIZE_URI__AND__FILTER_NORMALIZE_MEDIA_URI_ = function(ch) {
+  return soy.esc.$$ESCAPE_MAP_FOR_NORMALIZE_URI__AND__FILTER_NORMALIZE_URI__AND__FILTER_NORMALIZE_MEDIA_URI_[ch];
+};
+
+/**
+ * Matches characters that need to be escaped for the named directives.
+ * @private {!RegExp}
+ */
+soy.esc.$$MATCHER_FOR_NORMALIZE_HTML_ = /[\x00\x22\x27\x3c\x3e]/g;
+
+/**
+ * Matches characters that need to be escaped for the named directives.
+ * @private {!RegExp}
+ */
+soy.esc.$$MATCHER_FOR_ESCAPE_HTML_NOSPACE_ = /[\x00\x09-\x0d \x22\x26\x27\x2d\/\x3c-\x3e`\x85\xa0\u2028\u2029]/g;
+
+/**
+ * Matches characters that need to be escaped for the named directives.
+ * @private {!RegExp}
+ */
+soy.esc.$$MATCHER_FOR_NORMALIZE_HTML_NOSPACE_ = /[\x00\x09-\x0d \x22\x27\x2d\/\x3c-\x3e`\x85\xa0\u2028\u2029]/g;
+
+/**
+ * Matches characters that need to be escaped for the named directives.
+ * @private {!RegExp}
+ */
+soy.esc.$$MATCHER_FOR_ESCAPE_JS_STRING_ = /[\x00\x08-\x0d\x22\x26\x27\/\x3c-\x3e\x5b-\x5d\x7b\x7d\x85\u2028\u2029]/g;
+
+/**
+ * Matches characters that need to be escaped for the named directives.
+ * @private {!RegExp}
+ */
+soy.esc.$$MATCHER_FOR_ESCAPE_JS_REGEX_ = /[\x00\x08-\x0d\x22\x24\x26-\/\x3a\x3c-\x3f\x5b-\x5e\x7b-\x7d\x85\u2028\u2029]/g;
+
+/**
+ * Matches characters that need to be escaped for the named directives.
+ * @private {!RegExp}
+ */
+soy.esc.$$MATCHER_FOR_ESCAPE_CSS_STRING_ = /[\x00\x08-\x0d\x22\x26-\x2a\/\x3a-\x3e@\\\x7b\x7d\x85\xa0\u2028\u2029]/g;
+
+/**
+ * Matches characters that need to be escaped for the named directives.
+ * @private {!RegExp}
+ */
+soy.esc.$$MATCHER_FOR_NORMALIZE_URI__AND__FILTER_NORMALIZE_URI__AND__FILTER_NORMALIZE_MEDIA_URI_ = /[\x00- \x22\x27-\x29\x3c\x3e\\\x7b\x7d\x7f\x85\xa0\u2028\u2029\uff01\uff03\uff04\uff06-\uff0c\uff0f\uff1a\uff1b\uff1d\uff1f\uff20\uff3b\uff3d]/g;
+
+/**
+ * A pattern that vets values produced by the named directives.
+ * @private {!RegExp}
+ */
+soy.esc.$$FILTER_FOR_FILTER_CSS_VALUE_ = /^(?!-*(?:expression|(?:moz-)?binding))(?!\s+)(?:[.#]?-?(?:[_a-z0-9-]+)(?:-[_a-z0-9-]+)*-?|(?:rgb|hsl)a?\([0-9.%,\u0020]+\)|-?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[a-z]{1,2}|%)?|!important|\s+)*$/i;
+
+/**
+ * A pattern that vets values produced by the named directives.
+ * @private {!RegExp}
+ */
+soy.esc.$$FILTER_FOR_FILTER_NORMALIZE_URI_ = /^(?![^#?]*\/(?:\.|%2E){2}(?:[\/?#]|$))(?:(?:https?|mailto):|[^&:\/?#]*(?:[\/?#]|$))/i;
+
+/**
+ * A pattern that vets values produced by the named directives.
+ * @private {!RegExp}
+ */
+soy.esc.$$FILTER_FOR_FILTER_NORMALIZE_MEDIA_URI_ = /^[^&:\/?#]*(?:[\/?#]|$)|^https?:|^data:image\/[a-z0-9+]+;base64,[a-z0-9+\/]+=*$|^blob:/i;
+
+/**
+ * A pattern that vets values produced by the named directives.
+ * @private {!RegExp}
+ */
+soy.esc.$$FILTER_FOR_FILTER_IMAGE_DATA_URI_ = /^data:image\/(?:bmp|gif|jpe?g|png|tiff|webp);base64,[a-z0-9+\/]+=*$/i;
+
+/**
+ * A pattern that vets values produced by the named directives.
+ * @private {!RegExp}
+ */
+soy.esc.$$FILTER_FOR_FILTER_TEL_URI_ = /^tel:[0-9a-z;=\-+._!~*'\u0020\/():&$#?@,]+$/i;
+
+/**
+ * A pattern that vets values produced by the named directives.
+ * @private {!RegExp}
+ */
+soy.esc.$$FILTER_FOR_FILTER_HTML_ATTRIBUTES_ = /^(?!on|src|(?:style|action|archive|background|cite|classid|codebase|data|dsync|href|longdesc|usemap)\s*$)(?:[a-z0-9_$:-]*)$/i;
+
+/**
+ * A pattern that vets values produced by the named directives.
+ * @private {!RegExp}
+ */
+soy.esc.$$FILTER_FOR_FILTER_HTML_ELEMENT_NAME_ = /^(?!script|style|title|textarea|xmp|no)[a-z0-9_$:-]*$/i;
+
+/**
+ * A helper for the Soy directive |normalizeHtml
+ * @param {*} value Can be of any type but will be coerced to a string.
+ * @return {string} The escaped text.
+ */
+soy.esc.$$normalizeHtmlHelper = function(value) {
+  var str = String(value);
+  return str.replace(
+      soy.esc.$$MATCHER_FOR_NORMALIZE_HTML_,
+      soy.esc.$$REPLACER_FOR_NORMALIZE_HTML__AND__ESCAPE_HTML_NOSPACE__AND__NORMALIZE_HTML_NOSPACE_);
+};
+
+/**
+ * A helper for the Soy directive |escapeHtmlNospace
+ * @param {*} value Can be of any type but will be coerced to a string.
+ * @return {string} The escaped text.
+ */
+soy.esc.$$escapeHtmlNospaceHelper = function(value) {
+  var str = String(value);
+  return str.replace(
+      soy.esc.$$MATCHER_FOR_ESCAPE_HTML_NOSPACE_,
+      soy.esc.$$REPLACER_FOR_NORMALIZE_HTML__AND__ESCAPE_HTML_NOSPACE__AND__NORMALIZE_HTML_NOSPACE_);
+};
+
+/**
+ * A helper for the Soy directive |normalizeHtmlNospace
+ * @param {*} value Can be of any type but will be coerced to a string.
+ * @return {string} The escaped text.
+ */
+soy.esc.$$normalizeHtmlNospaceHelper = function(value) {
+  var str = String(value);
+  return str.replace(
+      soy.esc.$$MATCHER_FOR_NORMALIZE_HTML_NOSPACE_,
+      soy.esc.$$REPLACER_FOR_NORMALIZE_HTML__AND__ESCAPE_HTML_NOSPACE__AND__NORMALIZE_HTML_NOSPACE_);
+};
+
+/**
+ * A helper for the Soy directive |escapeJsString
+ * @param {*} value Can be of any type but will be coerced to a string.
+ * @return {string} The escaped text.
+ */
+soy.esc.$$escapeJsStringHelper = function(value) {
+  var str = String(value);
+  return str.replace(
+      soy.esc.$$MATCHER_FOR_ESCAPE_JS_STRING_,
+      soy.esc.$$REPLACER_FOR_ESCAPE_JS_STRING__AND__ESCAPE_JS_REGEX_);
+};
+
+/**
+ * A helper for the Soy directive |escapeJsRegex
+ * @param {*} value Can be of any type but will be coerced to a string.
+ * @return {string} The escaped text.
+ */
+soy.esc.$$escapeJsRegexHelper = function(value) {
+  var str = String(value);
+  return str.replace(
+      soy.esc.$$MATCHER_FOR_ESCAPE_JS_REGEX_,
+      soy.esc.$$REPLACER_FOR_ESCAPE_JS_STRING__AND__ESCAPE_JS_REGEX_);
+};
+
+/**
+ * A helper for the Soy directive |escapeCssString
+ * @param {*} value Can be of any type but will be coerced to a string.
+ * @return {string} The escaped text.
+ */
+soy.esc.$$escapeCssStringHelper = function(value) {
+  var str = String(value);
+  return str.replace(
+      soy.esc.$$MATCHER_FOR_ESCAPE_CSS_STRING_,
+      soy.esc.$$REPLACER_FOR_ESCAPE_CSS_STRING_);
+};
+
+/**
+ * A helper for the Soy directive |filterCssValue
+ * @param {*} value Can be of any type but will be coerced to a string.
+ * @return {string} The escaped text.
+ */
+soy.esc.$$filterCssValueHelper = function(value) {
+  var str = String(value);
+  if (!soy.esc.$$FILTER_FOR_FILTER_CSS_VALUE_.test(str)) {
+    goog.asserts.fail('Bad value `%s` for |filterCssValue', [str]);
+    return 'zSoyz';
+  }
+  return str;
+};
+
+/**
+ * A helper for the Soy directive |normalizeUri
+ * @param {*} value Can be of any type but will be coerced to a string.
+ * @return {string} The escaped text.
+ */
+soy.esc.$$normalizeUriHelper = function(value) {
+  var str = String(value);
+  return str.replace(
+      soy.esc.$$MATCHER_FOR_NORMALIZE_URI__AND__FILTER_NORMALIZE_URI__AND__FILTER_NORMALIZE_MEDIA_URI_,
+      soy.esc.$$REPLACER_FOR_NORMALIZE_URI__AND__FILTER_NORMALIZE_URI__AND__FILTER_NORMALIZE_MEDIA_URI_);
+};
+
+/**
+ * A helper for the Soy directive |filterNormalizeUri
+ * @param {*} value Can be of any type but will be coerced to a string.
+ * @return {string} The escaped text.
+ */
+soy.esc.$$filterNormalizeUriHelper = function(value) {
+  var str = String(value);
+  if (!soy.esc.$$FILTER_FOR_FILTER_NORMALIZE_URI_.test(str)) {
+    goog.asserts.fail('Bad value `%s` for |filterNormalizeUri', [str]);
+    return 'about:invalid#zSoyz';
+  }
+  return str.replace(
+      soy.esc.$$MATCHER_FOR_NORMALIZE_URI__AND__FILTER_NORMALIZE_URI__AND__FILTER_NORMALIZE_MEDIA_URI_,
+      soy.esc.$$REPLACER_FOR_NORMALIZE_URI__AND__FILTER_NORMALIZE_URI__AND__FILTER_NORMALIZE_MEDIA_URI_);
+};
+
+/**
+ * A helper for the Soy directive |filterNormalizeMediaUri
+ * @param {*} value Can be of any type but will be coerced to a string.
+ * @return {string} The escaped text.
+ */
+soy.esc.$$filterNormalizeMediaUriHelper = function(value) {
+  var str = String(value);
+  if (!soy.esc.$$FILTER_FOR_FILTER_NORMALIZE_MEDIA_URI_.test(str)) {
+    goog.asserts.fail('Bad value `%s` for |filterNormalizeMediaUri', [str]);
+    return 'about:invalid#zSoyz';
+  }
+  return str.replace(
+      soy.esc.$$MATCHER_FOR_NORMALIZE_URI__AND__FILTER_NORMALIZE_URI__AND__FILTER_NORMALIZE_MEDIA_URI_,
+      soy.esc.$$REPLACER_FOR_NORMALIZE_URI__AND__FILTER_NORMALIZE_URI__AND__FILTER_NORMALIZE_MEDIA_URI_);
+};
+
+/**
+ * A helper for the Soy directive |filterImageDataUri
+ * @param {*} value Can be of any type but will be coerced to a string.
+ * @return {string} The escaped text.
+ */
+soy.esc.$$filterImageDataUriHelper = function(value) {
+  var str = String(value);
+  if (!soy.esc.$$FILTER_FOR_FILTER_IMAGE_DATA_URI_.test(str)) {
+    goog.asserts.fail('Bad value `%s` for |filterImageDataUri', [str]);
+    return 'data:image/gif;base64,zSoyz';
+  }
+  return str;
+};
+
+/**
+ * A helper for the Soy directive |filterTelUri
+ * @param {*} value Can be of any type but will be coerced to a string.
+ * @return {string} The escaped text.
+ */
+soy.esc.$$filterTelUriHelper = function(value) {
+  var str = String(value);
+  if (!soy.esc.$$FILTER_FOR_FILTER_TEL_URI_.test(str)) {
+    goog.asserts.fail('Bad value `%s` for |filterTelUri', [str]);
+    return 'about:invalid#zSoyz';
+  }
+  return str;
+};
+
+/**
+ * A helper for the Soy directive |filterHtmlAttributes
+ * @param {*} value Can be of any type but will be coerced to a string.
+ * @return {string} The escaped text.
+ */
+soy.esc.$$filterHtmlAttributesHelper = function(value) {
+  var str = String(value);
+  if (!soy.esc.$$FILTER_FOR_FILTER_HTML_ATTRIBUTES_.test(str)) {
+    goog.asserts.fail('Bad value `%s` for |filterHtmlAttributes', [str]);
+    return 'zSoyz';
+  }
+  return str;
+};
+
+/**
+ * A helper for the Soy directive |filterHtmlElementName
+ * @param {*} value Can be of any type but will be coerced to a string.
+ * @return {string} The escaped text.
+ */
+soy.esc.$$filterHtmlElementNameHelper = function(value) {
+  var str = String(value);
+  if (!soy.esc.$$FILTER_FOR_FILTER_HTML_ELEMENT_NAME_.test(str)) {
+    goog.asserts.fail('Bad value `%s` for |filterHtmlElementName', [str]);
+    return 'zSoyz';
+  }
+  return str;
+};
+
+/**
+ * Matches all tags, HTML comments, and DOCTYPEs in tag soup HTML.
+ * By removing these, and replacing any '<' or '>' characters with
+ * entities we guarantee that the result can be embedded into a
+ * an attribute without introducing a tag boundary.
+ *
+ * @private {!RegExp}
+ */
+soy.esc.$$HTML_TAG_REGEX_ = /<(?:!|\/?([a-zA-Z][a-zA-Z0-9:\-]*))(?:[^>'"]|"[^"]*"|'[^']*')*>/g;
+
+/**
+ * Matches all occurrences of '<'.
+ *
+ * @private {!RegExp}
+ */
+soy.esc.$$LT_REGEX_ = /</g;
+
+/**
+ * Maps lower-case names of innocuous tags to true.
+ *
+ * @private {!Object<string, boolean>}
+ */
+soy.esc.$$SAFE_TAG_WHITELIST_ = {'b': true, 'br': true, 'em': true, 'i': true, 's': true, 'sub': true, 'sup': true, 'u': true};
+
+/**
+ * Pattern for matching attribute name and value, where value is single-quoted
+ * or double-quoted.
+ * See http://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0
+ *
+ * @private {!RegExp}
+ */
+soy.esc.$$HTML_ATTRIBUTE_REGEX_ = /([a-zA-Z][a-zA-Z0-9:\-]*)[\t\n\r\u0020]*=[\t\n\r\u0020]*("[^"]*"|'[^']*')/g;
+
+// END GENERATED CODE
diff --git a/third_party/ink/wireserializer.js b/third_party/ink/wireserializer.js
new file mode 100644
index 0000000..b88dcedf
--- /dev/null
+++ b/third_party/ink/wireserializer.js
@@ -0,0 +1,845 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+/**
+ * @fileoverview Protocol Buffer 2 Serializer which serializes and deserializes
+ * messages using the wire format. Note that this serializer requires protocol
+ * buffer reflection, which carries some overhead.
+ * @supported any browser with DataView implemented. For now Chrome9, FF15, IE10
+ *
+ * @see https://developers.google.com/protocol-buffers/docs/encoding
+ *
+ * TODO(feinberg): Replace goog.math.Long with mutable long representation that
+ * permits in-place arithmetic to avoid allocations.
+ */
+
+
+goog.provide('net.proto2.contrib.WireSerializer');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.math.Long');
+goog.require('goog.proto2.Message');
+goog.require('goog.proto2.Serializer');
+
+
+
+/**
+ * Wire format serializer.
+ *
+ * @constructor
+ * @extends {goog.proto2.Serializer}
+ */
+net.proto2.contrib.WireSerializer = function() {
+  /**
+   * This array is where proto bytes go during serialization.
+   * It must be reset for each serialization.
+   * @type {!Array.<number>}
+   * @private
+   */
+  this.buffer_ = [];
+
+  /**
+   * Scratch workspace to avoid allocations during serialization.
+   * @type {{value: number, length: number}}
+   * @private
+   */
+  this.scratchTag32_ = {value: 0, length: 0};
+
+  /**
+   * Scratch workspace to avoid allocations during serialization.
+   * @type {{value: !goog.math.Long, length: number}}
+   * @private
+   */
+  this.scratchTag64_ = {value: goog.math.Long.getZero(), length: 0};
+
+  /**
+   * Scratch data view for coding/decoding little-endian numbers.
+   * @type {!DataView}
+   * @private
+   */
+  this.dataView_ = new DataView(new ArrayBuffer(8));
+};
+goog.inherits(net.proto2.contrib.WireSerializer, goog.proto2.Serializer);
+
+
+/**
+ * @return {!Array.<number>} The serialized form of the message.
+ * @override
+ */
+net.proto2.contrib.WireSerializer.prototype.serialize = function(message) {
+  if (message == null) {
+    return [];
+  }
+
+  this.buffer_ = [];
+
+  var descriptor = message.getDescriptor();
+  var fields = descriptor.getFields();
+
+  // Add the known fields.
+  for (var i = 0; i < fields.length; i++) {
+    var field = fields[i];
+
+    if (!message.has(field)) {
+      continue;
+    }
+
+    if (field.isRepeated()) {
+      if (field.isPacked()) {
+        this.serializePackedField_(message, field);
+      } else {
+        for (var j = 0, n = message.countOf(field); j < n; j++) {
+          var val = message.get(field, j);
+          this.getSerializedValue(field, val);
+        }
+      }
+    } else {
+      this.getSerializedValue(field, message.get(field));
+    }
+  }
+
+  return this.buffer_;
+};
+
+
+/**
+ * Append the serialized packed field to our serialization buffer.
+ * @param {!goog.proto2.Message} message The message containing the field
+ *     to serialize.
+ * @param {!goog.proto2.FieldDescriptor} field The field to serialize.
+ * @return {boolean} Whether the field tag was serialized.
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.serializePackedField_ =
+    function(message, field) {
+  var buf = this.buffer_;
+
+  var wireType = 2;  // Per definition.
+
+  // Tag.
+  this.serializeVarint_((field.getTag() << 3) | wireType);
+
+  // Make note of the current buffer size. After serializing the repeated
+  // fields, splice the size header at the current position.
+  var savedBufferSize = buf.length;
+  for (var j = 0, n = message.countOf(field); j < n; j++) {
+    var val = message.get(field, j);
+    this.getSerializedValue(field, val, true /* omit tag */);
+  }
+  var serializedData = buf.splice(
+      savedBufferSize, buf.length - savedBufferSize);
+  this.serializeVarint_(serializedData.length);
+
+  var args = [buf.length, 0].concat(serializedData);
+  buf.splice.apply(buf, args);
+
+  return true;
+};
+
+
+/**
+ * Append the serialized field tag to our serialization buffer.
+ * @param {goog.proto2.FieldDescriptor} field The field to serialize.
+ * @return {boolean} Whether the field tag was serialized.
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.serializeFieldTag_ =
+    function(field) {
+  var wireType = 0;
+  switch (field.getFieldType()) {
+    default:
+      return false;
+    case goog.proto2.Message.FieldType.SINT32:
+    case goog.proto2.Message.FieldType.SINT64:
+    case goog.proto2.Message.FieldType.BOOL:
+    case goog.proto2.Message.FieldType.INT64:
+    case goog.proto2.Message.FieldType.ENUM:
+    case goog.proto2.Message.FieldType.INT32:
+    case goog.proto2.Message.FieldType.UINT32:
+    case goog.proto2.Message.FieldType.UINT64:
+      wireType = 0;
+      break;
+    case goog.proto2.Message.FieldType.FIXED64:
+    case goog.proto2.Message.FieldType.SFIXED64:
+    case goog.proto2.Message.FieldType.DOUBLE:
+      wireType = 1;
+      break;
+    case goog.proto2.Message.FieldType.STRING:
+    case goog.proto2.Message.FieldType.BYTES:
+    case goog.proto2.Message.FieldType.MESSAGE:
+      wireType = 2;
+      break;
+    case goog.proto2.Message.FieldType.GROUP:
+      wireType = 3;
+      break;
+    case goog.proto2.Message.FieldType.FIXED32:
+    case goog.proto2.Message.FieldType.SFIXED32:
+    case goog.proto2.Message.FieldType.FLOAT:
+      wireType = 5;
+      break;
+  }
+  this.serializeVarint_((field.getTag() << 3) | wireType);
+  return true;
+};
+
+
+/**
+ * Returns the serialized form of the given value for the given field if the
+ * field is a Message or Group and returns the value unchanged otherwise, except
+ * for Infinity, -Infinity and NaN numerical values which are converted to
+ * string representation.
+ *
+ * @param {goog.proto2.FieldDescriptor} field The field from which this
+ *     value came.
+ * @param {*} value The value of the field.
+ * @param {boolean=} opt_omitTag If present and true, do not serialize a field
+ *     tag.
+ *
+ * @return {*} The value.
+ * @protected
+ */
+net.proto2.contrib.WireSerializer.prototype.getSerializedValue =
+    function(field, value, opt_omitTag) {
+  if (!opt_omitTag) {
+    if (!this.serializeFieldTag_(field)) {
+      return false;
+    }
+  }
+
+  switch (field.getFieldType()) {
+    default:
+      throw new Error('Unknown field type ' + field.getFieldType());
+    case goog.proto2.Message.FieldType.SINT32:
+      this.serializeVarint_(this.zigZagEncode(/** @type {number} */ (value)));
+      break;
+    case goog.proto2.Message.FieldType.SINT64:
+      this.serializeVarint64_(this.zigZagEncode64_(
+          goog.math.Long.fromString(/** @type {string} */(value))));
+      break;
+    case goog.proto2.Message.FieldType.BOOL:
+      this.serializeVarint_(value ? 1 : 0);
+      break;
+    case goog.proto2.Message.FieldType.INT32:
+      var numericValue = /** @type {number} */ (value);
+      if (numericValue > 0) {
+        this.serializeVarint_(numericValue);
+      } else {
+        // Negative 32 bit quantities are always 10 bytes long.
+        this.serializeVarint64_(goog.math.Long.fromInt(numericValue));
+      }
+      break;
+    case goog.proto2.Message.FieldType.INT64:
+    case goog.proto2.Message.FieldType.UINT64:
+      this.serializeVarint64_(
+          goog.math.Long.fromString(/** @type {string} */(value)));
+      break;
+    case goog.proto2.Message.FieldType.ENUM:
+    case goog.proto2.Message.FieldType.UINT32:
+      this.serializeVarint_(/** @type {number} */ (value));
+      break;
+    case goog.proto2.Message.FieldType.FIXED64:
+    case goog.proto2.Message.FieldType.SFIXED64:
+      this.serializeFixed_(
+          goog.math.Long.fromString(/** @type {string} */ (value)), 8);
+      break;
+    case goog.proto2.Message.FieldType.DOUBLE:
+      this.serializeDouble_(/** @type {number} */ (value));
+      break;
+    case goog.proto2.Message.FieldType.STRING:
+      this.serializeString(value);
+      break;
+    case goog.proto2.Message.FieldType.BYTES:
+      this.serializeBytes(value);
+      break;
+    case goog.proto2.Message.FieldType.GROUP:
+      var serialized = new net.proto2.contrib.WireSerializer().serialize(
+          /** @type {goog.proto2.Message} */ (value));
+      goog.array.extend(this.buffer_, serialized);
+      this.serializeVarint_((field.getTag() << 3) | 4);
+      break;
+    case goog.proto2.Message.FieldType.MESSAGE:
+      var serialized = new net.proto2.contrib.WireSerializer().serialize(
+          /** @type {goog.proto2.Message} */ (value));
+      this.serializeVarint_(serialized.length);
+      goog.array.extend(this.buffer_, serialized);
+      break;
+    case goog.proto2.Message.FieldType.FIXED32:
+      this.serializeFixed_(
+          goog.math.Long.fromNumber(/** @type {number} */ (value)), 4);
+      break;
+    case goog.proto2.Message.FieldType.SFIXED32:
+      this.serializeFixed_(
+          goog.math.Long.fromInt(/** @type {number} */ (value)), 4);
+      break;
+    case goog.proto2.Message.FieldType.FLOAT:
+      this.serializeFloat_(/** @type {number} */ (value));
+      break;
+  }
+  // To avoid allocations, this method serializes into a pre-existing buffer,
+  // rather than serializing into a new value object.
+  return null;
+};
+
+
+/** @override */
+net.proto2.contrib.WireSerializer.prototype.deserializeTo =
+    function(message, buffer) {
+  if (buffer == null) {
+    // Since value double-equals null, it may be either null or undefined.
+    // Ensure we return the same one, since they have different meanings.
+    return buffer;
+  }
+
+  if (buffer instanceof ArrayBuffer) {
+    buffer = new Uint8Array(buffer);
+  }
+
+  var descriptor = message.getDescriptor();
+  var offset = 0;
+  var size = buffer.length;
+  var view = function() {
+    return buffer.subarray(offset);
+  };
+  // Because subarray is broken on ie10, we can't simply advance our view of the
+  // buffer. Instead, we keep track of an offset.
+  while (offset < buffer.length) {
+    var tag = this.parseUnsignedVarInt_(view());
+    var tagValue = tag.value;
+    var tagLength = tag.length;
+    var index = tagValue >> 3;
+    var wireType = tagValue & 0x7;  // Last 3 bits.
+
+    // Advance.
+    offset += tagLength;
+
+    var field = descriptor.findFieldByTag(index);
+    if (!field) {
+      // Unknown field; skip it.
+      offset += this.lengthForWireType_(wireType, view());
+      continue;
+    } else if (field.isPacked()) {  // Packed repeated.
+      // Read byte length.
+      var v = this.parseUnsignedVarInt_(view());
+      var remaining = v.value;
+      offset += v.length;
+      while (remaining > 0 && offset < buffer.length) {
+        var packedValue =
+            this.getDeserializedValue(field, view());
+        if (!packedValue) {
+          throw new Error('Expected ' + field.getFieldType());
+        }
+        message.add(field, packedValue.value);
+        offset += packedValue.length;
+        remaining -= packedValue.length;
+      }
+    } else {
+      var value = this.getDeserializedValue(field, view());
+      if (!value) {
+        throw new Error('Expected ' + field.getFieldType());
+      }
+      offset += value.length;
+      if (field.isRepeated()) {
+        message.add(field, value.value);
+      } else {
+        message.set(field, value.value);
+      }
+    }
+  }
+};
+
+
+/**
+ * @param {number} wireType
+ * @param {*} buffer The data of the message.
+ * @return {number} Default length to use for a given fieldType.
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.lengthForWireType_ = function(
+    wireType, buffer) {
+  var length = 0;
+  switch (wireType) {
+    case 0:  // int32, int64, uint32, uint64, sint32, sint64, bool, enum.
+      length = this.parseVarInt64_(buffer).length;
+      break;
+    case 1:  // fixed64, sfixed64, double.
+      length = 8;
+      break;
+    case 2:  // Length-delimited: string, bytes, messages, repeated fields.
+      var bufferLength = this.parseVarInt64_(buffer);
+      length = bufferLength.length + bufferLength.value.toInt();
+      break;
+    case 3:  // "Start group". Not supported.
+    case 4:  // "End group". Not supported.
+      goog.asserts.fail('Error deserializing group');
+      break;
+    case 5:  // fixed32, sfixed32, float.
+      length = 4;
+      break;
+  }
+  return length;
+};
+
+
+/**
+ * Deserializes a message from the expected format and places the
+ * data in the message. The message must correspond to a group. Moreover
+ * the buffer must be positioned after the initial START_GROUP tag for the
+ * group. The message will be terminated by the first END_GROUP tag at the
+ * same nesting level. It is the responsibility of the caller to validate that
+ * its field index matches the one in the opening START_GROUP tag. Since groups
+ * are not length-delimited, this method returns the length of the parsed
+ * data excluding the END_GROUP tag.
+ *
+ * @param {goog.proto2.Message} message The message in which to
+ *     place the information.
+ * @param {*} buffer The data of the message.
+ * @return {number} the length of the parsed message, excluding the closing tag.
+ * @protected
+ */
+ net.proto2.contrib.WireSerializer.prototype.deserializeGroupTo =
+    function(message, buffer) {
+  var descriptor = message.getDescriptor();
+  var parsedLength = 0;
+
+  while (true) {
+    var tag = this.parseUnsignedVarInt_(buffer);
+    var tagValue = tag.value;
+    var tagLength = tag.length;
+    var index = tagValue >> 3;
+    var wiretype = tagValue & 7;
+    if (wiretype == 4) {
+      // Got an end group.
+      break;
+    }
+    parsedLength += tagLength;
+    var value = {value: undefined, length: 0};
+    var field = descriptor.findFieldByTag(index);
+    if (field) {
+      value = this.getDeserializedValue(field, buffer.subarray(tagLength));
+      if (value && value.value !== null) {
+        if (field.isRepeated()) {
+          message.add(field, value.value);
+        } else {
+          message.set(field, value.value);
+        }
+      }
+    }
+    parsedLength += value.length;
+    if (buffer.length < tagLength + value.length) {
+      break;
+    }
+    buffer = buffer.subarray(tagLength + value.length);
+  }
+  return parsedLength;
+};
+
+
+/**
+ * @override
+ */
+net.proto2.contrib.WireSerializer.prototype.getDeserializedValue =
+    function(field, buffer) {
+  var value = null;
+  var t = field.getFieldType();
+  var varInt = this.parseVarInt64_(buffer);
+  var length = varInt.length;
+  switch (t) {
+    case goog.proto2.Message.FieldType.SINT32:
+      value = this.zigZagDecode_(varInt.value.toInt());
+      break;
+    case goog.proto2.Message.FieldType.SINT64:
+      value = this.zigZagDecode64_(varInt.value).toString();
+      break;
+    case goog.proto2.Message.FieldType.BOOL:
+      value = varInt.value.equals(goog.math.Long.getOne());
+      break;
+    case goog.proto2.Message.FieldType.INT64:
+    case goog.proto2.Message.FieldType.UINT64:
+      value = varInt.value.toString();
+      break;
+    case goog.proto2.Message.FieldType.INT32:
+      value = varInt.value.toInt();
+      break;
+    case goog.proto2.Message.FieldType.ENUM:
+    case goog.proto2.Message.FieldType.UINT32:
+      value = varInt.value.getLowBitsUnsigned();
+      break;
+    case goog.proto2.Message.FieldType.FIXED64:
+    case goog.proto2.Message.FieldType.SFIXED64:
+      value = this.parseFixed64_(buffer.subarray(0, 8)).toString();
+      length = 8;
+      break;
+    case goog.proto2.Message.FieldType.DOUBLE:
+      value = this.parseDouble_(buffer.subarray(0, 8));
+      length = 8;
+      break;
+    case goog.proto2.Message.FieldType.STRING:
+      var strBuffer =
+          buffer.subarray(varInt.length, varInt.length + varInt.value.toInt());
+      value = this.arrayBufferToUtf8String_(strBuffer);
+      length = varInt.length + varInt.value.toInt();
+      break;
+    case goog.proto2.Message.FieldType.BYTES:
+      var strBuffer =
+          buffer.subarray(varInt.length, varInt.length + varInt.value.toInt());
+      // Store the bytes using a String.
+      value = this.arrayBufferToString_(strBuffer);
+      length = varInt.length + varInt.value.toInt();
+      break;
+    case goog.proto2.Message.FieldType.GROUP:
+      value = field.getFieldMessageType().createMessageInstance();
+      var groupLength = this.deserializeGroupTo(value, buffer);
+      var next = buffer.subarray(groupLength);
+      var closingTag = this.parseVarInt64_(next);
+      var expected = (field.getTag() << 3) | 4;
+      goog.asserts.assert(closingTag.value.toInt() == expected,
+          'Error deserializing group');
+      length = groupLength + closingTag.length;
+      break;
+    case goog.proto2.Message.FieldType.MESSAGE:
+      length = varInt.length + varInt.value.toInt();
+      var data = buffer.subarray(varInt.length, length);
+      value = field.getFieldMessageType().createMessageInstance();
+      this.deserializeTo(value, data);
+      break;
+    case goog.proto2.Message.FieldType.FIXED32:
+    case goog.proto2.Message.FieldType.SFIXED32:
+      value = this.parseFixed32_(
+          buffer.subarray(0, 4), t == goog.proto2.Message.FieldType.SFIXED32);
+      length = 4;
+      break;
+    case goog.proto2.Message.FieldType.FLOAT:
+      value = this.parseFloat_(buffer.subarray(0, 4));
+      length = 4;
+      break;
+  }
+  return {value: value, length: length};
+};
+
+
+/**
+ * @param {*} value Binary string that needs to be converted to bytes.
+ */
+net.proto2.contrib.WireSerializer.prototype.serializeBytes = function(value) {
+  if (goog.isDefAndNotNull(value)) {
+    var valueStr = /** @type {string} */ (value);
+    // Serialize the number of bytes, per spec of the wire format.
+    this.serializeVarint_(valueStr.length);
+    for (var i = 0; i < valueStr.length; i++) {
+      this.buffer_.push(valueStr.charCodeAt(i));
+    }
+  }
+};
+
+
+/**
+ * @param {*} value String (possibly utf-8) that needs to be converted to bytes.
+ */
+net.proto2.contrib.WireSerializer.prototype.serializeString = function(value) {
+  if (goog.isDefAndNotNull(value)) {
+    var valueStr = /** @type {string} */ (value);
+    // Inspired by:
+    // http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
+    var utf8 = unescape(encodeURIComponent(valueStr));
+    // Serialize the length of the encoded string: what we want is the number
+    // of bytes, not the number of characters, per spec of the wire format.
+    this.serializeVarint_(utf8.length);
+    for (var i = 0; i < utf8.length; i++) {
+      this.buffer_.push(utf8.charCodeAt(i));
+    }
+  }
+};
+
+
+/**
+ * @param {*} buffer to parse as String.
+ * @return {{value: string, length: number}}
+ */
+net.proto2.contrib.WireSerializer.prototype.parseString = function(buffer) {
+  var length = this.parseUnsignedVarInt_(buffer);
+  var strBuffer = buffer.subarray(length.length, length.length + length.value);
+  return {
+    value: this.arrayBufferToUtf8String_(strBuffer),
+    length: length.length + length.value
+  };
+};
+
+
+/**
+ * @param {number} number signed number that needs to be converted to unsigned.
+ * @return {number}
+ */
+net.proto2.contrib.WireSerializer.prototype.zigZagEncode =
+    function(number) {
+  var sign = number >>> 31;
+  return (number << 1) ^ -sign;
+};
+
+
+/**
+ * @param {number} number Unsigned number in zigzag format that needs
+                   to be converted to signed.
+ * @return {number} signed.
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.zigZagDecode_ =
+    function(number) {
+  return (number >>> 1) ^ -(number & 1);
+};
+
+
+/**
+ * @param {!goog.math.Long} number signed number that needs to be converted to
+ * unsigned.
+ * @return {!goog.math.Long}
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.zigZagEncode64_ =
+    function(number) {
+  var sign = number.shiftRightUnsigned(63);
+  return number.shiftLeft(1).xor(sign.negate());
+};
+
+
+/**
+ * @param {!goog.math.Long} number Unsigned number in zigzag format that needs
+                   to be converted to signed.
+ * @return {!goog.math.Long}
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.zigZagDecode64_ =
+    function(number) {
+  return number.shiftRightUnsigned(1).xor(
+      number.and(goog.math.Long.getOne()).negate());
+};
+
+
+/**
+ * Serialize the given number as a varint into our buffer.
+ * @param {number} number that needs to be converted to varint.
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.serializeVarint_ =
+    function(number) {
+  do {
+    var chunk = number & 0x7F;
+    number = number >>> 7;
+    if (number > 0) {
+      chunk = chunk | 0x80;
+    }
+    this.buffer_.push(chunk);
+  } while (number > 0);
+};
+
+
+/**
+ * Serialize the given 64-bit number as a varint into our buffer.
+ * @param {!goog.math.Long} number that needs to be encoded as varint.
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.serializeVarint64_ =
+    function(number) {
+  var mask = goog.math.Long.fromInt(0x7F);
+  do {
+    var chunk = number.and(mask).toInt();
+    number = number.shiftRightUnsigned(7);
+    if (number.greaterThan(goog.math.Long.getZero())) {
+      chunk = chunk | 0x80;
+    }
+    this.buffer_.push(chunk);
+  } while (number.greaterThan(goog.math.Long.getZero()));
+};
+
+
+/**
+ * @param {*} buffer from which field number and type needs to be extracted.
+ * @return {{value: !goog.math.Long, length: number}}
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.parseVarInt64_ = function(buffer) {
+  var valueInfo = this.scratchTag64_;
+  var number = goog.math.Long.fromNumber(0);
+  var i = 0;
+  for (; i < buffer.length; i++) {
+    var bits = goog.math.Long.fromInt(buffer[i] & 0x7F).shiftLeft(i * 7);
+    number = number.or(bits);
+    if ((buffer[i] & 0x80) == 0) {
+      break;
+    }
+  }
+  valueInfo.value = number;
+  valueInfo.length = i + 1;
+  return valueInfo;
+};
+
+
+/**
+ * A special case parser for unsigned 32-bit varints, which can fit comfortably
+ * in 32 bits during decoding.
+ * @param {*} buffer from which field number and type needs to be extracted.
+ * @return {{value: number, length: number}}
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.parseUnsignedVarInt_ =
+    function(buffer) {
+  var valueInfo = this.scratchTag32_;
+  var result = 0;
+  var i = 0;
+  for (; i < buffer.length; i++) {
+    result = result | ((buffer[i] & 0x7F) << (i * 7));
+    if ((buffer[i] & 0x80) == 0) {
+      break;
+    }
+  }
+  valueInfo.value = result;
+  valueInfo.length = i + 1;
+  return valueInfo;
+};
+
+
+/**
+ * @param {goog.math.Long} number that needs to be converted to little endian
+ *     order.
+ * @param {number} size of the result array (4 = 32bit, 8 = 64bit).
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.serializeFixed_ =
+    function(number, size) {
+  var mask = goog.math.Long.fromInt(0xFF);
+  for (var i = 0; i < size; i++) {
+    var chunk = number.and(mask).toInt();
+    this.buffer_.push(chunk);
+    number = number.shiftRightUnsigned(8);
+  }
+};
+
+
+/**
+ * @param {*} buffer from which the fixed32 value needs to be extracted.
+ * @param {boolean} signed if the fixed32 value represents a signed value
+ *     (i.e. sfixed32).
+ * @return {number}
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.parseFixed32_ = function(
+    buffer, signed) {
+  var number = 0;
+  for (var i = 0; i < buffer.length; i++) {
+    number = number | (buffer[i] << (i * 8));
+  }
+  if (!signed) {
+    // The bitwise operations above treat numbers as signed int32 values.
+    // Correct for this in the unsigned case by using >>> to coerce to unsigned.
+    number = number >>> 0;
+  }
+  return number;
+};
+
+
+/**
+ * @param {*} buffer from which the fixed64 value needs to be extracted.
+ * @return {!goog.math.Long}
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.parseFixed64_ = function(buffer) {
+  // Javascript numbers are only accurate up to 51 bits as they are stored as
+  // 64-bit floating points. We store the result in a goog.math.Long object to
+  // preserve full precision.
+  return new goog.math.Long(
+      this.parseFixed32_(buffer.subarray(0, 4), true),
+      this.parseFixed32_(buffer.subarray(4, 8), true));
+};
+
+
+/**
+ * @param {*} buffer from which double needs to be extracted.
+ * @return {number}
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.parseDouble_ = function(buffer) {
+  for (var i = 0; i < 8; i++) {
+    this.dataView_.setUint8(i, buffer[i]);
+  }
+  return this.dataView_.getFloat64(0, true);  // little-endian
+};
+
+
+/**
+ * @param {*} buffer from which float needs to be extracted.
+ * @return {number}
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.parseFloat_ = function(buffer) {
+  for (var i = 0; i < 4; i++) {
+    this.dataView_.setUint8(i, buffer[i]);
+  }
+  return this.dataView_.getFloat32(0, true);  // little-endian
+};
+
+
+/**
+ * @param {number} number to be serialized to 8 bytes.
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.serializeDouble_ =
+    function(number) {
+  this.dataView_.setFloat64(0, number, true);  // little-endian
+  for (var i = 0; i < 8; i++) {
+    this.buffer_.push(this.dataView_.getUint8(i));
+  }
+};
+
+
+/**
+ * @param {number} number to be serialized to 4 bytes.
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.serializeFloat_ = function(number) {
+  this.dataView_.setFloat32(0, number, true);  // little-endian
+  for (var i = 0; i < 4; i++) {
+    this.buffer_.push(this.dataView_.getUint8(i));
+  }
+};
+
+
+/**
+ * This method converts an ArrayBuffer into a string (with utf8 encoding).
+ *
+ * @param {ArrayBuffer} buffer The buffer to convert to a string
+ * @return {string}
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.arrayBufferToUtf8String_ = function(
+    buffer) {
+  var str = this.arrayBufferToString_(buffer);
+  // Inspired by:
+  // http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
+  return decodeURIComponent(escape(str));
+};
+
+
+/**
+ * This method converts an ArrayBuffer into a string (each index is 1 byte).
+ *
+ * The maximum stack size in chrome is ~125k.  This means that using
+ * String.fromCharCode.apply will fail for strings larger than the maximum stack
+ * size.  This method breaks up the calls to fromCharCode into ~64k chunks to
+ * work around this limitation.
+ *
+ * @param {ArrayBuffer} buffer The buffer to convert to a string
+ * @return {string}
+ * @private
+ */
+net.proto2.contrib.WireSerializer.prototype.arrayBufferToString_ = function(
+    buffer) {
+  var CHUNK_SIZE = 65536;
+  var str = '';
+  var view = new Uint16Array(buffer);
+  for (var offset = 0; offset < view.length; offset += CHUNK_SIZE) {
+    var len = Math.min(CHUNK_SIZE, view.length - offset);
+    var subview = view.subarray(offset, offset + len);
+    str += String.fromCharCode.apply(null, subview);
+  }
+  return str;
+};
diff --git a/ui/login/display_manager.js b/ui/login/display_manager.js
index 1c4e80e..1c94df0 100644
--- a/ui/login/display_manager.js
+++ b/ui/login/display_manager.js
@@ -33,6 +33,7 @@
 /** @const */ var SCREEN_ARC_TERMS_OF_SERVICE = 'arc-tos';
 /** @const */ var SCREEN_WRONG_HWID = 'wrong-hwid';
 /** @const */ var SCREEN_DEVICE_DISABLED = 'device-disabled';
+/** @const */ var SCREEN_UPDATE_REQUIRED = 'update-required';
 /** @const */ var SCREEN_UNRECOVERABLE_CRYPTOHOME_ERROR =
     'unrecoverable-cryptohome-error';
 /** @const */ var SCREEN_ACTIVE_DIRECTORY_PASSWORD_CHANGE =
@@ -154,6 +155,7 @@
     SCREEN_ARC_TERMS_OF_SERVICE,
     SCREEN_WRONG_HWID,
     SCREEN_CONFIRM_PASSWORD,
+    SCREEN_UPDATE_REQUIRED,
     SCREEN_FATAL_ERROR
   ];
 
diff --git a/ui/login/md_screen_container.css b/ui/login/md_screen_container.css
index 708b08f..bdac3bb 100644
--- a/ui/login/md_screen_container.css
+++ b/ui/login/md_screen_container.css
@@ -97,6 +97,7 @@
 #oobe.terms-of-service #inner-container,
 #oobe.arc-tos #inner-container,
 #oobe.update #inner-container,
+#oobe.update-required #inner-container,
 #oobe.user-image #inner-container,
 #oobe.wrong-hwid #inner-container,
 #oobe.unrecoverable-cryptohome-error #inner-container {
@@ -186,6 +187,7 @@
 #arc-tos-dot,
 #tpm-error-message-dot,
 #wrong-hwid-dot,
+#update-required-dot,
 #unrecoverable-cryptohome-error-dot {
   display: none;
 }
diff --git a/ui/login/screen_container.css b/ui/login/screen_container.css
index 4ee8855..41833ce 100644
--- a/ui/login/screen_container.css
+++ b/ui/login/screen_container.css
@@ -88,6 +88,7 @@
 #oobe.terms-of-service #inner-container,
 #oobe.arc-tos #inner-container,
 #oobe.update #inner-container,
+#oobe.update-required #inner-container,
 #oobe.user-image #inner-container,
 #oobe.wrong-hwid #inner-container,
 #oobe.unrecoverable-cryptohome-error #inner-container {
@@ -176,6 +177,7 @@
 #terms-of-service-dot,
 #arc-tos-dot,
 #tpm-error-message-dot,
+#update-required-dot,
 #wrong-hwid-dot,
 #unrecoverable-cryptohome-error-dot {
   display: none;