[go: nahoru, domu]

Move overlay implementation to DevTools

This CL moves the implementation of inspect_tool_highlight.html to
DevTools, sets up a separate resource build target containing code
bundles produced by rollup. Also, it adds size checks and sets up
an example test. The future CLs will cover the rest of inspector tools
and better tests. A follow-up CL for Chromium to use the newly generated
resource is prepared[1].

[1] https://crrev.com/c/2194865

Bug: 1078267
Change-Id: I1c66e3c2aaec04c2f07fab8623d674ada95d8b24
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2192892
Commit-Queue: Alex Rudenko <alexrudenko@chromium.org>
Reviewed-by: Tim van der Lippe <tvanderlippe@chromium.org>
Reviewed-by: Mathias Bynens <mathias@chromium.org>
Reviewed-by: Peter Marshall <petermarshall@chromium.org>
diff --git a/BUILD.gn b/BUILD.gn
index 024977e..487704f 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -930,6 +930,7 @@
   "front_end/elements",
   "front_end/formatter_worker",
   "front_end/host",
+  "front_end/inspector_overlay:inspector_overlay",
   "front_end/issues:issues",
   "front_end/protocol_client",
   "front_end/root",
@@ -972,6 +973,10 @@
   public_deps = devtools_frontend_resources_deps
 }
 
+group("devtools_inspector_overlay_resources") {
+  public_deps = [ "front_end/inspector_overlay:build_inspector_overlay" ]
+}
+
 # Do not use this unless you need unpacked devtools at runtime.
 group("devtools_frontend_resources_data") {
   testonly = true
diff --git a/front_end/inspector_overlay/BUILD.gn b/front_end/inspector_overlay/BUILD.gn
new file mode 100644
index 0000000..a4c42d5
--- /dev/null
+++ b/front_end/inspector_overlay/BUILD.gn
@@ -0,0 +1,52 @@
+# Copyright 2020 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.
+
+import("../../third_party/typescript/typescript.gni")
+
+resources_out_dir = "$root_out_dir/resources/inspector_overlay"
+
+action("build_inspector_overlay") {
+  script = "../../scripts/build/build_inspector_overlay.py"
+
+  inputs = [
+    "./common.css",
+    "$target_gen_dir/common.js",
+    "$target_gen_dir/tool_highlight_impl.js",
+    "$target_gen_dir/tool_highlight.js",
+  ]
+
+  outputs = [
+    "$resources_out_dir/common.css",
+    "$resources_out_dir/tool_highlight.js",
+  ]
+
+  args = [
+    "common.css",
+    "1200",  # max_size in bytes
+    "tool_highlight.js",
+    "50000",  # max_size in bytes
+    "--input_path",
+    rebase_path(".", root_build_dir),
+    "--output_path",
+    rebase_path(resources_out_dir, root_build_dir),
+  ]
+
+  public_deps = [
+    ":copy_resources",
+    ":inspector_overlay",
+  ]
+}
+
+copy("copy_resources") {
+  sources = [ "resources.grd" ]
+  outputs = [ "$resources_out_dir/inspector_overlay_resources.grd" ]
+}
+
+ts_library("inspector_overlay") {
+  sources = [
+    "common.js",
+    "tool_highlight.js",
+    "tool_highlight_impl.js",
+  ]
+}
diff --git a/front_end/inspector_overlay/README.md b/front_end/inspector_overlay/README.md
new file mode 100644
index 0000000..c60d647
--- /dev/null
+++ b/front_end/inspector_overlay/README.md
@@ -0,0 +1,33 @@
+# Inspector Overlay
+
+Inspector Overlay provides JS/CSS modules which are responsible for rendering the overlay content when you inspect an element in DevTools.
+
+## How build works
+
+- Overlay modules are built using rollup and copied to $root_gen_dir.
+- `inspector_overlay_resources.grd` file is copied as well and it defines how modules are packaged in a `.pak` file.
+- The Chromium build uses `inspector_overlay_resources.grd` and produces a `.pak` file.
+- `inspector_overlay_agent.cc` extracts the modules and inlines them onto the overlay page.
+
+## Local Development
+
+To iterate on the overlay UI locally, start a web server in the root folder and open one of the `*_debug.html` files.
+
+For example:
+
+- `python -m SimpleHTTPServer 8000`
+- Go to `http://localhost:8000/front_end/inspector_overlay/tool_highlight_debug.html`
+
+In this mode, JS modules will be served without bundling.
+
+## Importing modules from DevTools
+
+It's possible to re-use modules from DevTools for the overlay implementation.
+To import a module, use the `import` statement with a relative path to the module.
+It's important to keep the size of the shipped overlay modules minimal so it's better to
+include only small standalone utilities. The build tooling will also check the size of the
+generated modules and notify you if they are too big.
+
+## Tests
+
+Overlay modules can be unit tested like other parts of DevTools. For an example, see `test/unittests/front_end/inspector_overlay/common_test.ts`.
diff --git a/front_end/inspector_overlay/common.css b/front_end/inspector_overlay/common.css
new file mode 100644
index 0000000..bc33865
--- /dev/null
+++ b/front_end/inspector_overlay/common.css
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019 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.
+ */
+
+body {
+  margin: 0;
+  padding: 0;
+  font-size: 13px;
+  color: #222;
+}
+
+body.platform-linux {
+  font-family: Roboto, Ubuntu, Arial, sans-serif;
+}
+
+body.platform-mac {
+  color: rgb(48, 57, 66);
+  font-family: '.SFNSDisplay-Regular', 'Helvetica Neue', 'Lucida Grande', sans-serif;
+}
+
+body.platform-windows {
+  font-family: 'Segoe UI', Tahoma, sans-serif;
+}
+
+.fill {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+
+#canvas {
+  pointer-events: none;
+}
+
+.hidden {
+  display: none !important;
+}
diff --git a/front_end/inspector_overlay/common.js b/front_end/inspector_overlay/common.js
new file mode 100644
index 0000000..2f6c0ab
--- /dev/null
+++ b/front_end/inspector_overlay/common.js
@@ -0,0 +1,120 @@
+// Copyright 2019 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.
+
+// @ts-nocheck
+// TODO(crbug.com/1011811): Enable TypeScript compiler checks
+
+window.viewportSize = {
+  width: 800,
+  height: 600
+};
+window.deviceScaleFactor = 1;
+window.emulationScaleFactor = 1;
+window.pageScaleFactor = 1;
+window.pageZoomFactor = 1;
+window.scrollX = 0;
+window.scrollY = 0;
+
+export function reset(resetData) {
+  window.viewportSize = resetData.viewportSize;
+  window.deviceScaleFactor = resetData.deviceScaleFactor;
+  window.pageScaleFactor = resetData.pageScaleFactor;
+  window.pageZoomFactor = resetData.pageZoomFactor;
+  window.emulationScaleFactor = resetData.emulationScaleFactor;
+  window.scrollX = Math.round(resetData.scrollX);
+  window.scrollY = Math.round(resetData.scrollY);
+
+  window.canvas = document.getElementById('canvas');
+  if (window.canvas) {
+    window.canvas.width = deviceScaleFactor * viewportSize.width;
+    window.canvas.height = deviceScaleFactor * viewportSize.height;
+    window.canvas.style.width = viewportSize.width + 'px';
+    window.canvas.style.height = viewportSize.height + 'px';
+
+    window.context = canvas.getContext('2d');
+    window.context.scale(deviceScaleFactor, deviceScaleFactor);
+
+    window.canvasWidth = viewportSize.width;
+    window.canvasHeight = viewportSize.height;
+  }
+
+  doReset();
+}
+
+function doReset() {
+}
+
+export function setPlatform(platform) {
+  window.platform = platform;
+  document.body.classList.add('platform-' + platform);
+}
+
+export function dispatch(message) {
+  const functionName = message.shift();
+  window[functionName].apply(null, message);
+}
+
+export function log(text) {
+  let element = document.getElementById('log');
+  if (!element) {
+    element = document.body.createChild();
+    element.id = 'log';
+  }
+  element.createChild('div').textContent = text;
+}
+
+export function eventHasCtrlOrMeta(event) {
+  return window.platform === 'mac' ? (event.metaKey && !event.ctrlKey) : (event.ctrlKey && !event.metaKey);
+}
+
+Element.prototype.createChild = function(tagName, className) {
+  const element = createElement(tagName, className);
+  element.addEventListener('click', function(e) {
+    e.stopPropagation();
+  }, false);
+  this.appendChild(element);
+  return element;
+};
+
+Element.prototype.createTextChild = function(text) {
+  const element = document.createTextNode(text);
+  this.appendChild(element);
+  return element;
+};
+
+Element.prototype.removeChildren = function() {
+  if (this.firstChild) {
+    this.textContent = '';
+  }
+};
+
+export function createElement(tagName, className) {
+  const element = document.createElement(tagName);
+  if (className) {
+    element.className = className;
+  }
+  return element;
+}
+
+String.prototype.trimEnd = function(maxLength) {
+  if (this.length <= maxLength) {
+    return String(this);
+  }
+  return this.substr(0, maxLength - 1) + '\u2026';
+};
+
+/**
+ * @param {number} num
+ * @param {number} min
+ * @param {number} max
+ * @return {number}
+ */
+Number.constrain = function(num, min, max) {
+  if (num < min) {
+    num = min;
+  } else if (num > max) {
+    num = max;
+  }
+  return num;
+};
diff --git a/front_end/inspector_overlay/resources.grd b/front_end/inspector_overlay/resources.grd
new file mode 100644
index 0000000..e348fc4
--- /dev/null
+++ b/front_end/inspector_overlay/resources.grd
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="0" current_release="1" output_all_resource_defines="false">
+  <outputs>
+    <output filename="grit/inspector_overlay_resources_map.h" type="rc_header">
+      <emit emit_type="prepend"></emit>
+    </output>
+    <output filename="inspector_overlay_resources.pak" type="data_package" />
+  </outputs>
+  <release seq="1">
+    <includes>
+      <include name="IDR_INSPECT_COMMON_CSS" file="common.css" type="BINDATA" compress="gzip"/>
+      <include name="IDR_INSPECT_TOOL_HIGHLIGHT_JS" file="tool_highlight.js" type="BINDATA" compress="gzip"/>
+    </includes>
+  </release>
+</grit>
diff --git a/front_end/inspector_overlay/tool_highlight.js b/front_end/inspector_overlay/tool_highlight.js
new file mode 100644
index 0000000..e1b8201
--- /dev/null
+++ b/front_end/inspector_overlay/tool_highlight.js
@@ -0,0 +1,290 @@
+// Copyright 2020 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.
+
+// @ts-nocheck
+// TODO(crbug.com/1011811): Enable TypeScript compiler checks
+
+import {dispatch, reset, setPlatform} from './common.js';
+import {doReset, drawHighlight} from './tool_highlight_impl.js';
+
+const style = `
+body {
+  --arrow-width: 15px;
+  --arrow-height: 8px;
+  --shadow-up: 5px;
+  --shadow-down: -5px;
+  --shadow-direction: var(--shadow-up);
+  --arrow-up: polygon(0 0, 100% 0, 50% 100%);
+  --arrow-down: polygon(50% 0, 0 100%, 100% 100%);
+  --arrow: var(--arrow-up);
+}
+
+.px {
+  color: rgb(128, 128, 128);
+}
+
+#element-title {
+  position: absolute;
+  z-index: 10;
+}
+
+/* Material */
+
+.tooltip-content {
+  position: absolute;
+  z-index: 10;
+  -webkit-user-select: none;
+}
+
+.tooltip-content {
+  background-color: white;
+  padding: 5px 8px;
+  border: 1px solid white;
+  border-radius: 3px;
+  box-sizing: border-box;
+  min-width: 100px;
+  max-width: min(300px, 100% - 4px);
+  z-index: 1;
+  background-clip: padding-box;
+  will-change: transform;
+  text-rendering: optimizeLegibility;
+  pointer-events: none;
+  filter: drop-shadow(0 2px 4px rgba(0,0,0,0.35));
+}
+
+.tooltip-content::after {
+  content: "";
+  background: white;
+  width: var(--arrow-width);
+  height: var(--arrow-height);
+  clip-path: var(--arrow);
+  position: absolute;
+  top: var(--arrow-top);
+  left: var(--arrow-left);
+  visibility: var(--arrow-visibility);
+}
+
+.element-info-section {
+  margin-top: 12px;
+  margin-bottom: 6px;
+}
+
+.section-name {
+  color: #333;
+  font-weight: 500;
+  font-size: 10px;
+  text-transform: uppercase;
+  letter-spacing: .05em;
+  line-height: 12px;
+}
+
+.element-info {
+  display: flex;
+  flex-direction: column;
+}
+
+.element-info-header {
+  display: flex;
+  align-items: baseline;
+}
+
+.element-info-body {
+  display: flex;
+  flex-direction: column;
+  padding-top: 2px;
+  margin-top: 2px;
+}
+
+.element-info-row {
+  display: flex;
+  line-height: 19px;
+}
+
+.separator-container {
+  display: flex;
+  align-items: center;
+  flex: auto;
+  margin-left: 7px;
+}
+
+.separator {
+  border-top: 1px solid #ddd;
+  width: 100%;
+}
+
+.element-info-name {
+  flex-shrink: 0;
+  color: #666;
+}
+
+.element-info-gap {
+  flex: auto;
+}
+
+.element-info-value-color {
+  display: flex;
+  color: rgb(48, 57, 66);
+  margin-left: 10px;
+  align-items: baseline;
+}
+
+.element-info-value-contrast {
+  display: flex;
+  align-items: center;
+  text-align: right;
+  color: rgb(48, 57, 66);
+  margin-left: 10px;
+}
+
+.element-info-value-contrast .a11y-icon {
+  margin-left: 8px;
+}
+
+.element-info-value-icon {
+  display: flex;
+  align-items: center;
+}
+
+.element-info-value-text {
+  text-align: right;
+  color: rgb(48, 57, 66);
+  margin-left: 10px;
+  align-items: baseline;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.color-swatch {
+  display: flex;
+  margin-right: 2px;
+  width: 10px;
+  height: 10px;
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
+  line-height: 10px;
+}
+
+.color-swatch-inner {
+  flex: auto;
+  border: 1px solid rgba(128, 128, 128, 0.6);
+}
+
+.element-description {
+  flex: 1 1;
+  font-weight: bold;
+  word-wrap: break-word;
+  word-break: break-all;
+}
+
+.dimensions {
+  color: hsl(0, 0%, 45%);
+  text-align: right;
+  margin-left: 10px;
+}
+
+.material-node-width {
+  margin-right: 2px;
+}
+
+.material-node-height {
+  margin-left: 2px;
+}
+
+.material-tag-name {
+  /* Keep this in sync with inspectorSyntaxHighlight.css (--dom-tag-name-color) */
+  color: rgb(136, 18, 128);
+}
+
+.material-class-name, .material-node-id {
+  /* Keep this in sync with inspectorSyntaxHighlight.css (.webkit-html-attribute-value) */
+  color: rgb(26, 26, 166);
+}
+
+.contrast-text {
+  width: 16px;
+  height: 16px;
+  text-align: center;
+  line-height: 16px;
+  margin-right: 8px;
+  border: 1px solid rgba(0, 0, 0, 0.1);
+  padding: 0 1px;
+}
+
+.a11y-icon {
+  width: 16px;
+  height: 16px;
+  background-repeat: no-repeat;
+  display: inline-block;
+}
+
+.a11y-icon-not-ok {
+  background-image: url('data:image/svg+xml,<svg fill="none" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="m9 1.5c-4.14 0-7.5 3.36-7.5 7.5s3.36 7.5 7.5 7.5 7.5-3.36 7.5-7.5-3.36-7.5-7.5-7.5zm0 13.5c-3.315 0-6-2.685-6-6 0-1.3875.4725-2.6625 1.2675-3.675l8.4075 8.4075c-1.0125.795-2.2875 1.2675-3.675 1.2675zm4.7325-2.325-8.4075-8.4075c1.0125-.795 2.2875-1.2675 3.675-1.2675 3.315 0 6 2.685 6 6 0 1.3875-.4725 2.6625-1.2675 3.675z" fill="%239e9e9e"/></svg>');
+}
+
+.a11y-icon-warning {
+  background-image: url('data:image/svg+xml,<svg fill="none" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="m8.25 11.25h1.5v1.5h-1.5zm0-6h1.5v4.5h-1.5zm.7425-3.75c-4.14 0-7.4925 3.36-7.4925 7.5s3.3525 7.5 7.4925 7.5c4.1475 0 7.5075-3.36 7.5075-7.5s-3.36-7.5-7.5075-7.5zm.0075 13.5c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6z" fill="%23e37400"/></svg>');
+}
+
+.a11y-icon-ok {
+  background-image: url('data:image/svg+xml,<svg fill="none" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="m9 1.5c-4.14 0-7.5 3.36-7.5 7.5s3.36 7.5 7.5 7.5 7.5-3.36 7.5-7.5-3.36-7.5-7.5-7.5zm0 13.5c-3.3075 0-6-2.6925-6-6s2.6925-6 6-6 6 2.6925 6 6-2.6925 6-6 6zm-1.5-4.35-1.95-1.95-1.05 1.05 3 3 6-6-1.05-1.05z" fill="%230ca40c"/></svg>');
+}
+
+@media (forced-colors: active) {
+  :root, body {
+      background-color: transparent;
+      forced-color-adjust: none;
+  }
+  .tooltip-content {
+      border-color: Highlight;
+      background-color: Canvas;
+      color: Text;
+      forced-color-adjust: none;
+  }
+  .tooltip-content::after {
+      background-color: Highlight;
+  }
+  .color-swatch-inner,
+  .contrast-text,
+  .separator {
+      border-color: Highlight;
+  }
+  .dimensions,
+  .element-info-name,
+  .element-info-value-color,
+  .element-info-value-contrast,
+  .element-info-value-icon,
+  .element-info-value-text,
+  .material-tag-name,
+  .material-class-name,
+  .material-node-id {
+      color: CanvasText;
+  }
+}`;
+
+
+window.setPlatform = function(platform) {
+  const styleTag = document.createElement('style');
+  styleTag.innerHTML = style;
+  document.head.append(styleTag);
+
+  document.body.classList.add('fill');
+
+  const canvas = document.createElement('canvas');
+  canvas.id = 'canvas';
+  canvas.classList.add('fill');
+  document.body.append(canvas);
+
+  const tooltip = document.createElement('div');
+  tooltip.id = 'tooltip-container';
+  document.body.append(tooltip);
+
+  setPlatform(platform);
+};
+
+window.reset = function(data) {
+  reset(data);
+  doReset(data);
+};
+window.drawHighlight = drawHighlight;
+window.dispatch = dispatch;
diff --git a/front_end/inspector_overlay/tool_highlight_debug.html b/front_end/inspector_overlay/tool_highlight_debug.html
new file mode 100644
index 0000000..ec36df2
--- /dev/null
+++ b/front_end/inspector_overlay/tool_highlight_debug.html
@@ -0,0 +1,27 @@
+<!--
+Copyright 2020 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.
+-->
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Debug</title>
+  <link rel="stylesheet" type="text/css" href="common.css">
+  <script type="module" src="tool_highlight.js"></script>
+</head>
+<body>
+  <script type="module">
+    function test() {
+        setPlatform("mac");
+        reset(window);
+        drawHighlight({"paths":[{"path":["M",122,133.796875,"L",822,133.796875,"L",822,208.796875,"L",122,208.796875,"Z"], "fillColor":"rgba(111, 168, 220, 0.658823529411765)","name":"content"},
+        {"path":["M",122,113.796875,"L",822,113.796875,"L",822,228.796875,"L",122,228.796875,"Z"],"fillColor":"rgba(246, 178, 107, 0.66)","name":"margin"}],"showRulers":false,"showExtensionLines":false,
+        "elementInfo":{"tagName":"button","className":"class.name", "idValue":"download-hero","nodeWidth":"700","nodeHeight":"75","style":{"color":"#FFFFFFFF","font-family":"\"Product Sans\", \"Open Sans\", Roboto, Arial, \"Product Sans\", \"Open Sans\", Roboto, Arial","font-size":"20px","line-height":"25px","padding":"0px","margin":"20px 0px","background-color":"#00000000"},"contrast":{"fontSize":"20px","fontWeight":"400","backgroundColor":"#F9B826BF"},"isKeyboardFocusable":false,"accessibleName":"name","accessibleRole":"role","showAccessibilityInfo":true}, showExtensionLines: true, showRulers: true, colorFormat: "hsl"});
+    }
+    test();
+  </script>
+</body>
+</html>
diff --git a/front_end/inspector_overlay/tool_highlight_impl.js b/front_end/inspector_overlay/tool_highlight_impl.js
new file mode 100644
index 0000000..7f919a4
--- /dev/null
+++ b/front_end/inspector_overlay/tool_highlight_impl.js
@@ -0,0 +1,864 @@
+// Copyright 2020 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.
+
+//  Copyright (C) 2012 Google Inc. All rights reserved.
+
+//  Redistribution and use in source and binary forms, with or without
+//  modification, are permitted provided that the following conditions
+//  are met:
+
+//  1.  Redistributions of source code must retain the above copyright
+//      notice, this list of conditions and the following disclaimer.
+//  2.  Redistributions in binary form must reproduce the above copyright
+//      notice, this list of conditions and the following disclaimer in the
+//      documentation and/or other materials provided with the distribution.
+//  3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+//      its contributors may be used to endorse or promote products derived
+//      from this software without specific prior written permission.
+
+//  THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+//  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+//  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//  DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+//  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+//  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+//  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+//  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+//  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+//  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+// @ts-nocheck
+// TODO(crbug.com/1011811): Enable TypeScript compiler checks
+
+import {createElement} from './common.js';
+
+const lightGridColor = 'rgba(0,0,0,0.2)';
+const darkGridColor = 'rgba(0,0,0,0.7)';
+const gridBackgroundColor = 'rgba(255, 255, 255, 0.8)';
+
+function _drawAxis(context, rulerAtRight, rulerAtBottom) {
+  if (window._gridPainted) {
+    return;
+  }
+  window._gridPainted = true;
+
+  context.save();
+
+  const pageFactor = pageZoomFactor * pageScaleFactor * emulationScaleFactor;
+  const scrollX = window.scrollX * pageScaleFactor;
+  const scrollY = window.scrollY * pageScaleFactor;
+  function zoom(x) {
+    return Math.round(x * pageFactor);
+  }
+  function unzoom(x) {
+    return Math.round(x / pageFactor);
+  }
+
+  const width = canvasWidth / pageFactor;
+  const height = canvasHeight / pageFactor;
+
+  const gridSubStep = 5;
+  const gridStep = 50;
+
+  {
+    // Draw X grid background
+    context.save();
+    context.fillStyle = gridBackgroundColor;
+    if (rulerAtBottom) {
+      context.fillRect(0, zoom(height) - 15, zoom(width), zoom(height));
+    } else {
+      context.fillRect(0, 0, zoom(width), 15);
+    }
+
+    // Clip out backgrounds intersection
+    context.globalCompositeOperation = 'destination-out';
+    context.fillStyle = 'red';
+    if (rulerAtRight) {
+      context.fillRect(zoom(width) - 15, 0, zoom(width), zoom(height));
+    } else {
+      context.fillRect(0, 0, 15, zoom(height));
+    }
+    context.restore();
+
+    // Draw Y grid background
+    context.fillStyle = gridBackgroundColor;
+    if (rulerAtRight) {
+      context.fillRect(zoom(width) - 15, 0, zoom(width), zoom(height));
+    } else {
+      context.fillRect(0, 0, 15, zoom(height));
+    }
+  }
+
+  context.lineWidth = 1;
+  context.strokeStyle = darkGridColor;
+  context.fillStyle = darkGridColor;
+  {
+    // Draw labels.
+    context.save();
+    context.translate(-scrollX, 0.5 - scrollY);
+    const maxY = height + unzoom(scrollY);
+    for (let y = 2 * gridStep; y < maxY; y += 2 * gridStep) {
+      context.save();
+      context.translate(scrollX, zoom(y));
+      context.rotate(-Math.PI / 2);
+      context.fillText(y, 2, rulerAtRight ? zoom(width) - 7 : 13);
+      context.restore();
+    }
+    context.translate(0.5, -0.5);
+    const maxX = width + unzoom(scrollX);
+    for (let x = 2 * gridStep; x < maxX; x += 2 * gridStep) {
+      context.save();
+      context.fillText(x, zoom(x) + 2, rulerAtBottom ? scrollY + zoom(height) - 7 : scrollY + 13);
+      context.restore();
+    }
+    context.restore();
+  }
+
+  {
+    // Draw vertical grid
+    context.save();
+    if (rulerAtRight) {
+      context.translate(zoom(width), 0);
+      context.scale(-1, 1);
+    }
+    context.translate(-scrollX, 0.5 - scrollY);
+    const maxY = height + unzoom(scrollY);
+    for (let y = gridStep; y < maxY; y += gridStep) {
+      context.beginPath();
+      context.moveTo(scrollX, zoom(y));
+      const markLength = (y % (gridStep * 2)) ? 5 : 8;
+      context.lineTo(scrollX + markLength, zoom(y));
+      context.stroke();
+    }
+    context.strokeStyle = lightGridColor;
+    for (let y = gridSubStep; y < maxY; y += gridSubStep) {
+      if (!(y % gridStep)) {
+        continue;
+      }
+      context.beginPath();
+      context.moveTo(scrollX, zoom(y));
+      context.lineTo(scrollX + gridSubStep, zoom(y));
+      context.stroke();
+    }
+    context.restore();
+  }
+
+  {
+    // Draw horizontal grid
+    context.save();
+    if (rulerAtBottom) {
+      context.translate(0, zoom(height));
+      context.scale(1, -1);
+    }
+    context.translate(0.5 - scrollX, -scrollY);
+    const maxX = width + unzoom(scrollX);
+    for (let x = gridStep; x < maxX; x += gridStep) {
+      context.beginPath();
+      context.moveTo(zoom(x), scrollY);
+      const markLength = (x % (gridStep * 2)) ? 5 : 8;
+      context.lineTo(zoom(x), scrollY + markLength);
+      context.stroke();
+    }
+    context.strokeStyle = lightGridColor;
+    for (let x = gridSubStep; x < maxX; x += gridSubStep) {
+      if (!(x % gridStep)) {
+        continue;
+      }
+      context.beginPath();
+      context.moveTo(zoom(x), scrollY);
+      context.lineTo(zoom(x), scrollY + gridSubStep);
+      context.stroke();
+    }
+    context.restore();
+  }
+
+  context.restore();
+}
+
+export function doReset() {
+  document.getElementById('tooltip-container').removeChildren();
+  window._gridPainted = false;
+}
+
+/**
+ * @param {!String} hexa
+ * @return {!Array<number>}
+ */
+function parseHexa(hexa) {
+  return hexa.match(/#(\w\w)(\w\w)(\w\w)(\w\w)/).slice(1).map(c => parseInt(c, 16) / 255);
+}
+
+/**
+ * TODO(alexrudenko): import this and other color helpers from DevTools
+ * @param {!Array<number>} rgba
+ * @return {!Array<number>}
+ */
+function rgbaToHsla(rgba) {
+  const [r, g, b] = rgba;
+  const max = Math.max(r, g, b);
+  const min = Math.min(r, g, b);
+  const diff = max - min;
+  const sum = max + min;
+
+  let h;
+  if (min === max) {
+    h = 0;
+  } else if (r === max) {
+    h = ((1 / 6 * (g - b) / diff) + 1) % 1;
+  } else if (g === max) {
+    h = (1 / 6 * (b - r) / diff) + 1 / 3;
+  } else {
+    h = (1 / 6 * (r - g) / diff) + 2 / 3;
+  }
+
+  const l = 0.5 * sum;
+
+  let s;
+  if (l === 0) {
+    s = 0;
+  } else if (l === 1) {
+    s = 0;
+  } else if (l <= 0.5) {
+    s = diff / sum;
+  } else {
+    s = diff / (2 - sum);
+  }
+
+  return [h, s, l, rgba[3]];
+}
+
+/**
+ * @param {!String} hexa
+ * @param {!String} colorFormat
+ * @return {!String}
+ */
+function formatColor(hexa, colorFormat) {
+  if (colorFormat === 'rgb') {
+    const [r, g, b, a] = parseHexa(hexa);
+    // rgb(r g b [ / a])
+    return `rgb(${(r * 255).toFixed()} ${(g * 255).toFixed()} ${(b * 255).toFixed()}${
+        a === 1 ? '' : ' / ' + Math.round(a * 100) / 100})`;
+  }
+
+  if (colorFormat === 'hsl') {
+    const [h, s, l, a] = rgbaToHsla(parseHexa(hexa));
+    // hsl(hdeg s l [ / a])
+    return `hsl(${Math.round(h * 360)}deg ${Math.round(s * 100)} ${Math.round(l * 100)}${
+        a === 1 ? '' : ' / ' + Math.round(a * 100) / 100})`;
+  }
+
+  if (hexa.endsWith('FF')) {
+    // short hex if no alpha
+    return hexa.substr(0, 7);
+  }
+
+  return hexa;
+}
+
+/**
+* Calculate the contrast ratio between a foreground and a background color.
+* Returns the ratio to 1, for example for two colors with a contrast ratio of 21:1,
+* this function will return 21.
+* See http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
+* @param {!Array<number>} fgRGBA
+* @param {!Array<number>} bgRGBA
+* @return {number}
+*/
+function contrastRatio(fgRGBA, bgRGBA) {
+  // If we have a semi-transparent background color over an unknown
+  // background, draw the line for the "worst case" scenario: where
+  // the unknown background is the same color as the text.
+  bgRGBA = blendColors(bgRGBA, fgRGBA);
+  const fgLuminance = luminance(blendColors(fgRGBA, bgRGBA));
+  const bgLuminance = luminance(bgRGBA);
+  const result = (Math.max(fgLuminance, bgLuminance) + 0.05) / (Math.min(fgLuminance, bgLuminance) + 0.05);
+  return result.toFixed(2);
+
+  /**
+    * Calculate the luminance of this color using the WCAG algorithm.
+    * See http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
+    * @param {!Array<number>} rgba
+    * @return {number}
+    */
+  function luminance(rgba) {
+    const rSRGB = rgba[0];
+    const gSRGB = rgba[1];
+    const bSRGB = rgba[2];
+
+    const r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow(((rSRGB + 0.055) / 1.055), 2.4);
+    const g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow(((gSRGB + 0.055) / 1.055), 2.4);
+    const b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow(((bSRGB + 0.055) / 1.055), 2.4);
+
+    return 0.2126 * r + 0.7152 * g + 0.0722 * b;
+  }
+
+  /**
+    * Combine the two given color according to alpha blending.
+    * @param {!Array<number>} fgRGBA
+    * @param {!Array<number>} bgRGBA
+    * @return {!Array<number>}
+    */
+  function blendColors(fgRGBA, bgRGBA) {
+    const alpha = fgRGBA[3];
+    return [
+      ((1 - alpha) * bgRGBA[0]) + (alpha * fgRGBA[0]), ((1 - alpha) * bgRGBA[1]) + (alpha * fgRGBA[1]),
+      ((1 - alpha) * bgRGBA[2]) + (alpha * fgRGBA[2]), alpha + (bgRGBA[3] * (1 - alpha))
+    ];
+  }
+}
+
+function computeIsLargeFont(contrast) {
+  const boldWeights = new Set(['bold', 'bolder', '600', '700', '800', '900']);
+
+  const fontSizePx = parseFloat(contrast.fontSize.replace('px', ''));
+  const isBold = boldWeights.has(contrast.fontWeight);
+
+  const fontSizePt = fontSizePx * 72 / 96;
+  if (isBold) {
+    return fontSizePt >= 14;
+  }
+  return fontSizePt >= 18;
+}
+
+function _createElementDescription(elementInfo, colorFormat) {
+  const elementInfoElement = createElement('div', 'element-info');
+  const elementInfoHeaderElement = elementInfoElement.createChild('div', 'element-info-header');
+  const descriptionElement = elementInfoHeaderElement.createChild('div', 'element-description monospace');
+  const tagNameElement = descriptionElement.createChild('span', 'material-tag-name');
+  tagNameElement.textContent = elementInfo.tagName;
+  const nodeIdElement = descriptionElement.createChild('span', 'material-node-id');
+  const maxLength = 80;
+  nodeIdElement.textContent = elementInfo.idValue ? '#' + elementInfo.idValue.trimEnd(maxLength) : '';
+  nodeIdElement.classList.toggle('hidden', !elementInfo.idValue);
+
+  const classNameElement = descriptionElement.createChild('span', 'material-class-name');
+  if (nodeIdElement.textContent.length < maxLength) {
+    classNameElement.textContent = (elementInfo.className || '').trimEnd(maxLength - nodeIdElement.textContent.length);
+  }
+  classNameElement.classList.toggle('hidden', !elementInfo.className);
+  const dimensionsElement = elementInfoHeaderElement.createChild('div', 'dimensions');
+  dimensionsElement.createChild('span', 'material-node-width').textContent =
+      Math.round(elementInfo.nodeWidth * 100) / 100;
+  dimensionsElement.createTextChild('\u00d7');
+  dimensionsElement.createChild('span', 'material-node-height').textContent =
+      Math.round(elementInfo.nodeHeight * 100) / 100;
+
+  const style = elementInfo.style || {};
+  let elementInfoBodyElement;
+
+  if (elementInfo.isLockedAncestor) {
+    addTextRow('Showing the locked ancestor', '');
+  }
+
+  const color = style['color'];
+  if (color && color !== '#00000000') {
+    addColorRow('Color', color, colorFormat);
+  }
+
+  const fontFamily = style['font-family'];
+  const fontSize = style['font-size'];
+  if (fontFamily && fontSize !== '0px') {
+    addTextRow('Font', `${fontSize} ${fontFamily}`);
+  }
+
+  const bgcolor = style['background-color'];
+  if (bgcolor && bgcolor !== '#00000000') {
+    addColorRow('Background', bgcolor, colorFormat);
+  }
+
+  const margin = style['margin'];
+  if (margin && margin !== '0px') {
+    addTextRow('Margin', margin);
+  }
+
+  const padding = style['padding'];
+  if (padding && padding !== '0px') {
+    addTextRow('Padding', padding);
+  }
+
+  const cbgColor = elementInfo.contrast ? elementInfo.contrast.backgroundColor : null;
+  const hasContrastInfo = color && color !== '#00000000' && cbgColor && cbgColor !== '#00000000';
+
+  if (elementInfo.showAccessibilityInfo) {
+    addSection('Accessibility');
+
+    if (hasContrastInfo) {
+      addContrastRow(style['color'], elementInfo.contrast);
+    }
+
+    addTextRow('Name', elementInfo.accessibleName);
+    addTextRow('Role', elementInfo.accessibleRole);
+    addIconRow(
+        'Keyboard-focusable',
+        elementInfo.isKeyboardFocusable ? 'a11y-icon a11y-icon-ok' : 'a11y-icon a11y-icon-not-ok');
+  }
+
+  function ensureElementInfoBody() {
+    if (!elementInfoBodyElement) {
+      elementInfoBodyElement = elementInfoElement.createChild('div', 'element-info-body');
+    }
+  }
+
+  function addSection(name) {
+    ensureElementInfoBody();
+    const rowElement = elementInfoBodyElement.createChild('div', 'element-info-row element-info-section');
+    const nameElement = rowElement.createChild('div', 'section-name');
+    nameElement.textContent = name;
+    rowElement.createChild('div', 'separator-container').createChild('div', 'separator');
+  }
+
+  function addRow(name, rowClassName, valueClassName) {
+    ensureElementInfoBody();
+    const rowElement = elementInfoBodyElement.createChild('div', 'element-info-row');
+    if (rowClassName) {
+      rowElement.classList.add(rowClassName);
+    }
+    const nameElement = rowElement.createChild('div', 'element-info-name');
+    nameElement.textContent = name;
+    rowElement.createChild('div', 'element-info-gap');
+    return rowElement.createChild('div', valueClassName || '');
+  }
+
+  function addIconRow(name, value) {
+    addRow(name, '', 'element-info-value-icon').createChild('div', value);
+  }
+
+  function addTextRow(name, value) {
+    addRow(name, '', 'element-info-value-text').createTextChild(value);
+  }
+
+  function addColorRow(name, color, colorFormat) {
+    const valueElement = addRow(name, '', 'element-info-value-color');
+    const swatch = valueElement.createChild('div', 'color-swatch');
+    const inner = swatch.createChild('div', 'color-swatch-inner');
+    inner.style.backgroundColor = color;
+    valueElement.createTextChild(formatColor(color, colorFormat));
+  }
+
+  function addContrastRow(fgColor, contrast) {
+    const ratio = contrastRatio(parseHexa(fgColor), parseHexa(contrast.backgroundColor));
+    const threshold = computeIsLargeFont(contrast) ? 3.0 : 4.5;
+    const valueElement = addRow('Contrast', '', 'element-info-value-contrast');
+    const sampleText = valueElement.createChild('div', 'contrast-text');
+    sampleText.style.color = fgColor;
+    sampleText.style.backgroundColor = contrast.backgroundColor;
+    sampleText.textContent = 'Aa';
+    const valueSpan = valueElement.createChild('span');
+    valueSpan.textContent = Math.round(ratio * 100) / 100;
+    valueElement.createChild('div', ratio < threshold ? 'a11y-icon a11y-icon-warning' : 'a11y-icon a11y-icon-ok');
+  }
+
+  return elementInfoElement;
+}
+
+function _drawElementTitle(elementInfo, bounds, colorFormat) {
+  const tooltipContainer = document.getElementById('tooltip-container');
+  tooltipContainer.removeChildren();
+  _createMaterialTooltip(tooltipContainer, bounds, _createElementDescription(elementInfo, colorFormat), true);
+}
+
+function _createMaterialTooltip(parentElement, bounds, contentElement, withArrow) {
+  const tooltipContainer = parentElement.createChild('div');
+  const tooltipContent = tooltipContainer.createChild('div', 'tooltip-content');
+  tooltipContent.appendChild(contentElement);
+
+  const titleWidth = tooltipContent.offsetWidth;
+  const titleHeight = tooltipContent.offsetHeight;
+  const arrowHalfWidth = 8;
+  const pageMargin = 2;
+  const arrowWidth = arrowHalfWidth * 2;
+  const arrowInset = arrowHalfWidth + 2;
+
+  const containerMinX = pageMargin + arrowInset;
+  const containerMaxX = canvasWidth - pageMargin - arrowInset - arrowWidth;
+
+  // Left align arrow to the tooltip but ensure it is pointing to the element.
+  // Center align arrow if the inspected element bounds are too narrow.
+  const boundsAreTooNarrow = bounds.maxX - bounds.minX < arrowWidth + 2 * arrowInset;
+  let arrowX;
+  if (boundsAreTooNarrow) {
+    arrowX = (bounds.minX + bounds.maxX) * 0.5 - arrowHalfWidth;
+  } else {
+    const xFromLeftBound = bounds.minX + arrowInset;
+    const xFromRightBound = bounds.maxX - arrowInset - arrowWidth;
+    if (xFromLeftBound > containerMinX && xFromLeftBound < containerMaxX) {
+      arrowX = xFromLeftBound;
+    } else {
+      arrowX = Number.constrain(containerMinX, xFromLeftBound, xFromRightBound);
+    }
+  }
+  // Hide arrow if element is completely off the sides of the page.
+  const arrowHidden = !withArrow || arrowX < containerMinX || arrowX > containerMaxX;
+
+  let boxX = arrowX - arrowInset;
+  boxX = Number.constrain(boxX, pageMargin, canvasWidth - titleWidth - pageMargin);
+
+  let boxY = bounds.minY - arrowHalfWidth - titleHeight;
+  let >
+  if (boxY < 0) {
+    boxY = Math.min(canvasHeight - titleHeight, bounds.maxY + arrowHalfWidth);
+    >
+  } else if (bounds.minY > canvasHeight) {
+    boxY = canvasHeight - arrowHalfWidth - titleHeight;
+  }
+
+  // If tooltip intersects with the bounds, hide it.
+  // Allow bounds to contain the box though for the large elements like <body>.
+  const includes = boxX >= bounds.minX && boxX + titleWidth <= bounds.maxX && boxY >= bounds.minY &&
+      boxY + titleHeight <= bounds.maxY;
+  const overlaps =
+      boxX < bounds.maxX && boxX + titleWidth > bounds.minX && boxY < bounds.maxY && boxY + titleHeight > bounds.minY;
+  if (overlaps && !includes) {
+    tooltipContent.style.display = 'none';
+    return;
+  }
+
+  tooltipContent.style.top = boxY + 'px';
+  tooltipContent.style.left = boxX + 'px';
+  tooltipContent.style.setProperty('--arrow-visibility', (arrowHidden || includes) ? 'hidden' : 'visible');
+  if (arrowHidden) {
+    return;
+  }
+
+  tooltipContent.style.setProperty('--arrow', onTop ? 'var(--arrow-up)' : 'var(--arrow-down)');
+  tooltipContent.style.setProperty('--shadow-direction', onTop ? 'var(--shadow-up)' : 'var(--shadow-down)');
+  tooltipContent.style.setProperty('--arrow-top', (onTop ? titleHeight : -arrowHalfWidth) + 'px');
+  tooltipContent.style.setProperty('--arrow-left', (arrowX - boxX) + 'px');
+}
+
+function _drawRulers(context, bounds, rulerAtRight, rulerAtBottom, color, dash) {
+  context.save();
+  const width = canvasWidth;
+  const height = canvasHeight;
+  context.strokeStyle = color || 'rgba(128, 128, 128, 0.3)';
+  context.lineWidth = 1;
+  context.translate(0.5, 0.5);
+  if (dash) {
+    context.setLineDash([3, 3]);
+  }
+
+  if (rulerAtRight) {
+    for (const y in bounds.rightmostXForY) {
+      context.beginPath();
+      context.moveTo(width, y);
+      context.lineTo(bounds.rightmostXForY[y], y);
+      context.stroke();
+    }
+  } else {
+    for (const y in bounds.leftmostXForY) {
+      context.beginPath();
+      context.moveTo(0, y);
+      context.lineTo(bounds.leftmostXForY[y], y);
+      context.stroke();
+    }
+  }
+
+  if (rulerAtBottom) {
+    for (const x in bounds.bottommostYForX) {
+      context.beginPath();
+      context.moveTo(x, height);
+      context.lineTo(x, bounds.topmostYForX[x]);
+      context.stroke();
+    }
+  } else {
+    for (const x in bounds.topmostYForX) {
+      context.beginPath();
+      context.moveTo(x, 0);
+      context.lineTo(x, bounds.topmostYForX[x]);
+      context.stroke();
+    }
+  }
+
+  context.restore();
+}
+
+function buildPath(commands, bounds) {
+  let commandsIndex = 0;
+
+  function extractPoints(count) {
+    const points = [];
+
+    for (let i = 0; i < count; ++i) {
+      const x = Math.round(commands[commandsIndex++] * emulationScaleFactor);
+      bounds.maxX = Math.max(bounds.maxX, x);
+      bounds.minX = Math.min(bounds.minX, x);
+
+      const y = Math.round(commands[commandsIndex++] * emulationScaleFactor);
+      bounds.maxY = Math.max(bounds.maxY, y);
+      bounds.minY = Math.min(bounds.minY, y);
+
+      bounds.leftmostXForY[y] = Math.min(bounds.leftmostXForY[y] || Number.MAX_VALUE, x);
+      bounds.rightmostXForY[y] = Math.max(bounds.rightmostXForY[y] || Number.MIN_VALUE, x);
+      bounds.topmostYForX[x] = Math.min(bounds.topmostYForX[x] || Number.MAX_VALUE, y);
+      bounds.bottommostYForX[x] = Math.max(bounds.bottommostYForX[x] || Number.MIN_VALUE, y);
+      points.push(x, y);
+    }
+    return points;
+  }
+
+  const commandsLength = commands.length;
+  const path = new Path2D();
+  while (commandsIndex < commandsLength) {
+    switch (commands[commandsIndex++]) {
+      case 'M':
+        path.moveTo.apply(path, extractPoints(1));
+        break;
+      case 'L':
+        path.lineTo.apply(path, extractPoints(1));
+        break;
+      case 'C':
+        path.bezierCurveTo.apply(path, extractPoints(3));
+        break;
+      case 'Q':
+        path.quadraticCurveTo.apply(path, extractPoints(2));
+        break;
+      case 'Z':
+        path.closePath();
+        break;
+    }
+  }
+  path.closePath();
+  return path;
+}
+
+function drawPath(context, commands, fillColor, outlineColor, bounds) {
+  context.save();
+  const path = buildPath(commands, bounds);
+  if (fillColor) {
+    context.fillStyle = fillColor;
+    context.fill(path);
+  }
+  if (outlineColor) {
+    context.lineWidth = 2;
+    context.strokeStyle = outlineColor;
+    context.stroke(path);
+  }
+  context.restore();
+  return path;
+}
+
+function emptyBounds() {
+  const bounds = {
+    minX: Number.MAX_VALUE,
+    minY: Number.MAX_VALUE,
+    maxX: Number.MIN_VALUE,
+    maxY: Number.MIN_VALUE,
+    leftmostXForY: {},
+    rightmostXForY: {},
+    topmostYForX: {},
+    bottommostYForX: {}
+  };
+  return bounds;
+}
+
+function _drawLayoutGridHighlight(highlight, context) {
+  // Draw Grid border
+  if (highlight.gridHighlightConfig.gridBorderColor) {
+    context.save();
+    context.translate(0.5, 0.5);
+    context.lineWidth = 0;
+    if (highlight.gridHighlightConfig.gridBorderDash) {
+      context.setLineDash([3, 3]);
+    }
+    context.strokeStyle = highlight.gridHighlightConfig.gridBorderColor;
+    context.stroke(buildPath(highlight.gridBorder, emptyBounds()));
+    context.restore();
+  }
+
+  // Draw Cell Border
+  if (highlight.gridHighlightConfig.cellBorderColor) {
+    const rowBounds = emptyBounds();
+    const columnBounds = emptyBounds();
+    const rowPath = buildPath(highlight.rows, rowBounds);
+    const columnPath = buildPath(highlight.columns, columnBounds);
+    context.save();
+    context.translate(0.5, 0.5);
+    if (highlight.gridHighlightConfig.cellBorderDash) {
+      context.setLineDash([3, 3]);
+    }
+    context.lineWidth = 0;
+    context.strokeStyle = highlight.gridHighlightConfig.cellBorderColor;
+
+    context.save();
+    context.clip(columnPath);
+    context.stroke(rowPath);
+    context.restore();
+
+    context.save();
+    context.clip(rowPath);
+    context.stroke(columnPath);
+    context.restore();
+
+    context.restore();
+
+    if (highlight.gridHighlightConfig.showGridExtensionLines) {
+      // Extend row gap lines left/up.
+      _drawRulers(
+          context, rowBounds, /* rulerAtRight */ false, /* rulerAtBottom */ false,
+          highlight.gridHighlightConfig.cellBorderColor, highlight.gridHighlightConfig.cellBorderDash);
+      // Extend row gap right/down.
+      _drawRulers(
+          context, rowBounds, /* rulerAtRight */ true, /* rulerAtBottom */ true,
+          highlight.gridHighlightConfig.cellBorderColor, highlight.gridHighlightConfig.cellBorderDash);
+      // Extend column lines left/up.
+      _drawRulers(
+          context, columnBounds, /* rulerAtRight */ false, /* rulerAtBottom */ false,
+          highlight.gridHighlightConfig.cellBorderColor, highlight.gridHighlightConfig.cellBorderDash);
+      // Extend column right/down.
+      _drawRulers(
+          context, columnBounds, /* rulerAtRight */ true, /* rulerAtBottom */ true,
+          highlight.gridHighlightConfig.cellBorderColor, highlight.gridHighlightConfig.cellBorderDash);
+    }
+  }
+
+  // Row Gaps
+  if (highlight.gridHighlightConfig.rowGapColor) {
+    context.save();
+    context.translate(0.5, 0.5);
+    context.lineWidth = 0;
+    context.fillStyle = highlight.gridHighlightConfig.rowGapColor;
+    const bounds = emptyBounds();
+    const path = buildPath(highlight.rowGaps, bounds);
+    if (highlight.gridHighlightConfig.rowHatchColor) {
+      hatchFillPath(context, path, bounds, 10, highlight.gridHighlightConfig.rowHatchColor, /* flipDirection */ true);
+    }
+    context.fill(path);
+    context.restore();
+  }
+
+  // Column Gaps
+  if (highlight.gridHighlightConfig.columnGapColor) {
+    context.save();
+    context.translate(0.5, 0.5);
+    context.lineWidth = 0;
+    context.fillStyle = highlight.gridHighlightConfig.columnGapColor;
+    const bounds = emptyBounds();
+    const path = buildPath(highlight.columnGaps, bounds);
+    if (highlight.gridHighlightConfig.columnHatchColor) {
+      hatchFillPath(context, path, bounds, 10, highlight.gridHighlightConfig.columnHatchColor);
+    }
+    context.fill(path);
+    context.restore();
+  }
+}
+
+/**
+ * Draw line hatching at a 45 degree angle for a given
+ * path.
+ *   __________
+ *   |\  \  \ |
+ *   | \  \  \|
+ *   |  \  \  |
+ *   |\  \  \ |
+ *   **********
+ *
+ * @param {CanvasRenderingContext2D} context
+ * @param {Path2D} path
+ * @param {Object} bounds
+ * @param {delta} delta - vertical gap between hatching lines in pixels
+ * @param {string} color
+ * @param {boolean=} flipDirection - lines are drawn from top right to bottom left
+ *
+ */
+function hatchFillPath(context, path, bounds, delta, color, flipDirection) {
+  const dx = bounds.maxX - bounds.minX;
+  const dy = bounds.maxY - bounds.minY;
+  context.rect(bounds.minX, bounds.minY, dx, dy);
+  context.save();
+  context.clip(path);
+  context.setLineDash([5, 3]);
+  const majorAxis = Math.max(dx, dy);
+  context.strokeStyle = color;
+  if (flipDirection) {
+    for (let i = -majorAxis; i < majorAxis; i += delta) {
+      context.beginPath();
+      context.moveTo(bounds.maxX - i, bounds.minY);
+      context.lineTo(bounds.maxX - dy - i, bounds.maxY);
+      context.stroke();
+    }
+  } else {
+    for (let i = -majorAxis; i < majorAxis; i += delta) {
+      context.beginPath();
+      context.moveTo(i + bounds.minX, bounds.minY);
+      context.lineTo(dy + i + bounds.minX, bounds.maxY);
+      context.stroke();
+    }
+  }
+  context.restore();
+}
+
+function clipLayoutGridCells(highlight, context) {
+  // It may seem simpler to, before drawing the desired path, call context.clip()
+  // with the rows and then with the columns. However, the 2nd context.clip() call
+  // would try to find the intersection of the rows and columns, which is way too
+  // expensive if the grid is huge, e.g. a 1000x1000 grid has 1M cells.
+  // Therefore, it's better to draw the path first, set the globalCompositeOperation
+  // so that the existing canvas content is kept where it overlaps with new content,
+  // and then draw the rows and columns.
+  if (highlight.gridInfo) {
+    for (const grid of highlight.gridInfo) {
+      if (!grid.isPrimaryGrid) {
+        continue;
+      }
+      context.save();
+      context.globalCompositeOperation = 'destination-in';
+      drawPath(context, grid.rows, 'red', null, emptyBounds());
+      drawPath(context, grid.columns, 'red', null, emptyBounds());
+      context.restore();
+    }
+  }
+}
+
+export function drawHighlight(highlight, context) {
+  context = context || window.context;
+  context.save();
+
+  const bounds = emptyBounds();
+
+  for (let paths = highlight.paths.slice(); paths.length;) {
+    const path = paths.pop();
+    context.save();
+    drawPath(context, path.path, path.fillColor, path.outlineColor, bounds);
+    if (paths.length) {
+      context.globalCompositeOperation = 'destination-out';
+      drawPath(context, paths[paths.length - 1].path, 'red', null, bounds);
+    }
+    // Clip content quad using the data grid cells info to create white stripes.
+    if (path.name === 'content') {
+      clipLayoutGridCells(highlight, context);
+    }
+    context.restore();
+  }
+  context.restore();
+
+  context.save();
+
+  const rulerAtRight =
+      highlight.paths.length && highlight.showRulers && bounds.minX < 20 && bounds.maxX + 20 < canvasWidth;
+  const rulerAtBottom =
+      highlight.paths.length && highlight.showRulers && bounds.minY < 20 && bounds.maxY + 20 < canvasHeight;
+
+  if (highlight.showRulers) {
+    _drawAxis(context, rulerAtRight, rulerAtBottom);
+  }
+
+  if (highlight.paths.length) {
+    if (highlight.showExtensionLines) {
+      _drawRulers(context, bounds, rulerAtRight, rulerAtBottom);
+    }
+
+    if (highlight.elementInfo) {
+      _drawElementTitle(highlight.elementInfo, bounds, highlight.colorFormat);
+    }
+  }
+  if (highlight.gridInfo) {
+    for (const grid of highlight.gridInfo) {
+      _drawLayoutGridHighlight(grid, context);
+    }
+  }
+  context.restore();
+
+  return {bounds: bounds};
+}
diff --git a/scripts/build/build_inspector_overlay.py b/scripts/build/build_inspector_overlay.py
new file mode 100644
index 0000000..39a703b
--- /dev/null
+++ b/scripts/build/build_inspector_overlay.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+#
+# Copyright 2020 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.
+"""
+Builds inspector overlay:
+- bundles input js files using rollup
+- copies css files as is
+"""
+
+from os import path
+from os.path import join
+from modular_build import read_file, write_file
+from itertools import tee
+
+import os
+import sys
+import subprocess
+import rjsmin
+
+try:
+    original_sys_path = sys.path
+    sys.path = sys.path + [
+        path.join(os.path.dirname(os.path.realpath(__file__)), '..')
+    ]
+    import devtools_paths
+finally:
+    sys.path = original_sys_path
+
+
+def check_size(filename, data, max_size):
+    assert len(
+        data
+    ) < max_size, "generated file %s should not exceed max_size of %d bytes. Current size: %d" % (
+        filename, max_size, len(data))
+
+
+def rollup(input_path, output_path, filename, max_size):
+    target = join(input_path, filename)
+    rollup_process = subprocess.Popen(
+        [devtools_paths.node_path(),
+         devtools_paths.rollup_path()] +
+        ['--format', 'iife', '-n', 'InspectorOverlay'] + ['--input', target],
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE)
+    out, error = rollup_process.communicate()
+    if not out:
+        raise Exception("rollup failed: " + error)
+    min = rjsmin.jsmin(out)
+    check_size(filename, min, max_size)
+    write_file(join(output_path, filename), min)
+
+
+def to_pairs(list):
+    pairs = []
+    while list:
+        pairs.append((list.pop(0), list.pop(0)))
+    return pairs
+
+
+def main(argv):
+    try:
+        input_path_flag_index = argv.index('--input_path')
+        input_path = argv[input_path_flag_index + 1]
+        output_path_flag_index = argv.index('--output_path')
+        output_path = argv[output_path_flag_index + 1]
+
+        file_names_with_sizes = to_pairs(argv[1:input_path_flag_index])
+        for filename, max_size in file_names_with_sizes:
+            max_size = int(max_size)
+            if filename.endswith(".js"):
+                rollup(input_path, output_path, filename, max_size)
+            if filename.endswith(".css"):
+                css_file = read_file(join(input_path, filename))
+                check_size(filename, css_file, max_size)
+                write_file(join(output_path, filename), css_file)
+
+    except:
+        print(
+            'Usage: %s filename_1 max_size_1 filename_2 max_size_2 ... filename_N max_size_N --input_path <input_path> --output_path <output_path>'
+            % argv[0])
+        raise
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
diff --git a/scripts/localization/check_localizability.js b/scripts/localization/check_localizability.js
index 39af5a0..88c1cc5 100644
--- a/scripts/localization/check_localizability.js
+++ b/scripts/localization/check_localizability.js
@@ -279,6 +279,9 @@
 
 async function auditFileForLocalizability(filePath, errors) {
   const fileContent = await localizationUtils.parseFileContent(filePath);
+  if (path.extname(filePath) === '.grd') {
+    return;
+  }
   if (path.extname(filePath) === '.grdp') {
     return auditGrdpFile(filePath, fileContent, errors);
   }
diff --git a/test/unittests/front_end/BUILD.gn b/test/unittests/front_end/BUILD.gn
index 942664b..6bc249d 100644
--- a/test/unittests/front_end/BUILD.gn
+++ b/test/unittests/front_end/BUILD.gn
@@ -13,6 +13,7 @@
     "elements",
     "formatter_worker",
     "inline_editor",
+    "inspector_overlay",
     "issues",
     "persistence",
     "platform",
diff --git a/test/unittests/front_end/inspector_overlay/BUILD.gn b/test/unittests/front_end/inspector_overlay/BUILD.gn
new file mode 100644
index 0000000..8a394a2
--- /dev/null
+++ b/test/unittests/front_end/inspector_overlay/BUILD.gn
@@ -0,0 +1,8 @@
+import("../../../../third_party/typescript/typescript.gni")
+
+ts_library("inspector_overlay") {
+  testonly = true
+  sources = [ "common_test.ts" ]
+
+  deps = [ "../../../../front_end/inspector_overlay" ]
+}
diff --git a/test/unittests/front_end/inspector_overlay/common_test.ts b/test/unittests/front_end/inspector_overlay/common_test.ts
new file mode 100644
index 0000000..ee9ce33
--- /dev/null
+++ b/test/unittests/front_end/inspector_overlay/common_test.ts
@@ -0,0 +1,13 @@
+// Copyright 2020 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.
+
+const {assert} = chai;
+
+import {createElement} from '../../../../front_end/inspector_overlay/common.js';
+
+describe('common', () => {
+  it('createElement', () => {
+    assert.instanceOf(createElement('div', 'test'), HTMLDivElement);
+  });
+});